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 已存在http://cfc.iwintrue.com192.168.16.251:3306/cfcFiles:
tests/e2e/seed-data.sqltests/e2e/reset-db.sh这一步创建种子数据和重置脚本。种子数据包含每个角色的标准测试账号。
编写种子数据,包含以下测试账号:
| 角色 | 手机号 | 用途 |
|---|---|---|
| 管理员 | 13800138000 | admin 端登录 |
| 家长 | 13701366188 | mini-app 家长操作 |
| 孩子1 | 13701366189 | 某家庭的孩子 |
| 孩子2 | 13701366190 | 某家庭的第二个孩子 |
| 规划师 | 13800000001 | teacher 端操作 |
| 营养师 | 13800000010 | nutritionist 端操作 |
| 管家 | 13800000020 | butler 端操作 |
| 测评师 | 13800000030 | 测评师子角色 |
| 供应商 | 13800138001 | vendor 端操作 |
包含以下基础数据:
1 个已完成订单
-- 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
#!/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
chmod +x tests/e2e/reset-db.sh
[ ] Step 3: 验证重置脚本
bash tests/e2e/reset-db.sh
Expected: 数据库重置成功,测试用户和基础数据可用。
Files:
tests/e2e/helpers/auth.js统一8类角色的登录逻辑。admin 走 web 管理端登录页面,其他角色走 mini-app 登录流程。
[ ] Step 1: 创建 helpers/auth.js
// 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
创建一个简单的验证脚本:
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个角色名称打印正确。
Files:
tests/e2e/helpers/utils.jsCreate: tests/e2e/helpers/db.js
[ ] Step 1: 创建 helpers/utils.js
// 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
// 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
// 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'],
],
};
Files:
Create: tests/e2e/helpers/fixtures.js
[ ] Step 1: 创建 helpers/fixtures.js
// 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 };
Files:
tests/e2e/roles/parent/family-management.spec.jstests/e2e/roles/parent/wish-management.spec.jstests/e2e/roles/parent/assessment-order.spec.jstests/e2e/roles/parent/energy-dashboard.spec.jstests/e2e/roles/parent/points.spec.js从现有文件迁移并升级,加入认证 + API 断言。
[ ] Step 1: 创建 family-management.spec.js
// 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
// 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
// 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
// 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
// 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();
});
});
Files:
tests/e2e/roles/child/login.spec.jstests/e2e/roles/child/daily-checkin.spec.jsCreate: tests/e2e/roles/child/mini-games.spec.js
[ ] Step 1: 创建 roles/child/login.spec.js
// 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
// 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
// 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);
});
});
Files:
tests/e2e/roles/teacher/login.spec.jsCreate: tests/e2e/roles/teacher/family-binding.spec.js
[ ] Step 1: 创建 roles/teacher/login.spec.js
// 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
// 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);
});
});
Files:
tests/e2e/roles/admin/login.spec.jstests/e2e/roles/admin/article/workflow.spec.jsCreate: tests/e2e/roles/admin/vendor/management.spec.js
[ ] Step 1: 创建 roles/admin/login.spec.js
// 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
// 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
// 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);
}
});
});
[ ] Step 1: 重置数据库
bash tests/e2e/reset-db.sh
Expected: 所有种子数据写入成功。
[ ] Step 2: 运行首批测试
npx playwright test tests/e2e/roles/parent/family-management.spec.js --project=desktop
Expected: 测试通过,API 断言通过。
[ ] Step 3: 运行所有 Phase 1 测试
npx playwright test tests/e2e/roles/ --project=desktop
Expected: 所有角色测试通过。
[ ] Step 4: 提交 Phase 1 代码
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
"
| 检查项 | 状态 |
|---|---|
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 |
□ |
| 全部测试通过 | □ |