|
@@ -0,0 +1,1379 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * ================================================================
|
|
|
|
|
+ * FLOW-LEVEL L1 TEST SUITE — 流程级一级测试用例
|
|
|
|
|
+ * ================================================================
|
|
|
|
|
+ * Based on docs/flows/*.md flow diagrams
|
|
|
|
|
+ * Tests each flow's key endpoints per role
|
|
|
|
|
+ *
|
|
|
|
|
+ * Roles: parent, child, teacher, admin, vendor
|
|
|
|
|
+ *
|
|
|
|
|
+ * Usage:
|
|
|
|
|
+ * node tests/e2e/flow-l1-tests.js
|
|
|
|
|
+ * ================================================================
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+var http = require('http');
|
|
|
|
|
+var https = require('https');
|
|
|
|
|
+var querystring = require('querystring');
|
|
|
|
|
+var path = require('path');
|
|
|
|
|
+var child_process = require('child_process');
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// Configuration
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+var BASE_URL = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com';
|
|
|
|
|
+var MYSQL_HOST = process.env.MYSQL_HOST || '192.168.16.251';
|
|
|
|
|
+var MYSQL_USER = process.env.MYSQL_USER || 'zxyj';
|
|
|
|
|
+var MYSQL_PASS = process.env.MYSQL_PASS || 'zxyj@123';
|
|
|
|
|
+var MYSQL_DB = process.env.MYSQL_DB || 'zxyj';
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// Test Results Collector
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+var results = {
|
|
|
|
|
+ passed: 0,
|
|
|
|
|
+ failed: 0,
|
|
|
|
|
+ skipped: 0,
|
|
|
|
|
+ issues: [],
|
|
|
|
|
+ details: []
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function log(msg) {
|
|
|
|
|
+ console.log('[TEST] ' + msg);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function record(flow, role, step, passed, detail) {
|
|
|
|
|
+ var icon = passed ? '✅' : '❌';
|
|
|
|
|
+ var line = icon + ' [' + flow + '] ' + role + ' | ' + step + (detail ? ' — ' + detail : '');
|
|
|
|
|
+ console.log(line);
|
|
|
|
|
+ results.details.push({ flow: flow, role: role, step: step, passed: passed, detail: detail });
|
|
|
|
|
+ if (passed) results.passed++;
|
|
|
|
|
+ else results.failed++;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function skip(flow, role, step, reason) {
|
|
|
|
|
+ var line = '⏭️ [' + flow + '] ' + role + ' | ' + step + ' — SKIPPED: ' + reason;
|
|
|
|
|
+ console.log(line);
|
|
|
|
|
+ results.details.push({ flow: flow, role: role, step: step, passed: null, detail: 'SKIP: ' + reason });
|
|
|
|
|
+ results.skipped++;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function issue(flow, role, step, desc) {
|
|
|
|
|
+ results.issues.push({ flow: flow, role: role, step: step, description: desc });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// HTTP Request Helper (plain Node.js, no deps)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+function apiCall(method, endpoint, data, token) {
|
|
|
|
|
+ return new Promise(function(resolve) {
|
|
|
|
|
+ var url = new URL(endpoint, BASE_URL);
|
|
|
|
|
+ var options = {
|
|
|
|
|
+ hostname: url.hostname,
|
|
|
|
|
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
|
|
|
+ path: url.pathname + url.search,
|
|
|
|
|
+ method: method,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ timeout: 15000
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (token) {
|
|
|
|
|
+ options.headers['Authorization'] = 'Bearer ' + token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var body = data ? JSON.stringify(data) : null;
|
|
|
|
|
+ if (body) {
|
|
|
|
|
+ options.headers['Content-Length'] = Buffer.byteLength(body);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var transport = url.protocol === 'https:' ? https : http;
|
|
|
|
|
+
|
|
|
|
|
+ var req = transport.request(options, function(res) {
|
|
|
|
|
+ var chunks = [];
|
|
|
|
|
+ res.on('data', function(chunk) { chunks.push(chunk); });
|
|
|
|
|
+ res.on('end', function() {
|
|
|
|
|
+ var rawBody = Buffer.concat(chunks).toString();
|
|
|
|
|
+ var parsed = null;
|
|
|
|
|
+ try { parsed = JSON.parse(rawBody); } catch(e) { parsed = null; }
|
|
|
|
|
+ resolve({
|
|
|
|
|
+ status: res.statusCode,
|
|
|
|
|
+ body: parsed,
|
|
|
|
|
+ raw: rawBody
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ req.on('error', function(err) {
|
|
|
|
|
+ resolve({ status: 0, body: null, raw: err.message });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ req.on('timeout', function() {
|
|
|
|
|
+ req.destroy();
|
|
|
|
|
+ resolve({ status: 0, body: null, raw: 'TIMEOUT' });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (body) req.write(body);
|
|
|
|
|
+ req.end();
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// DB Helper - insert verification codes
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+function insertCode(phone) {
|
|
|
|
|
+ var type = phone === '13800138000' ? 'admin_login' : 'login';
|
|
|
|
|
+ try {
|
|
|
|
|
+ child_process.execSync(
|
|
|
|
|
+ 'mysql -h ' + MYSQL_HOST + ' -u ' + MYSQL_USER + ' -p"' + MYSQL_PASS + '" ' + MYSQL_DB +
|
|
|
|
|
+ ' -e "INSERT INTO verification_codes (phone, code, type, expires_in, created_at, expires_at, used) VALUES (\'' + phone + '\', \'123456\', \'' + type + '\', 300, NOW(), \'2026-12-31 23:59:59\', 0);"',
|
|
|
|
|
+ { stdio: 'ignore', shell: true, timeout: 10000 }
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch(e) {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// Login Helper
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+var tokens = {};
|
|
|
|
|
+
|
|
|
|
|
+async function loginAll() {
|
|
|
|
|
+ var roles = {
|
|
|
|
|
+ parent: { phone: '13701366188', method: 'code' },
|
|
|
|
|
+ child: { phone: '13701366189', method: 'code' },
|
|
|
|
|
+ teacher: { phone: '13800000001', method: 'code' },
|
|
|
|
|
+ admin: { phone: '13800138000', method: 'password' },
|
|
|
|
|
+ vendor: { phone: '13800138001', method: 'code' }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (var role in roles) {
|
|
|
|
|
+ var cfg = roles[role];
|
|
|
|
|
+ insertCode(cfg.phone);
|
|
|
|
|
+ insertCode(cfg.phone);
|
|
|
|
|
+ insertCode(cfg.phone);
|
|
|
|
|
+
|
|
|
|
|
+ var resp;
|
|
|
|
|
+ if (cfg.method === 'password') {
|
|
|
|
|
+ resp = await apiCall('POST', '/api/admin-auth/login-by-password', {
|
|
|
|
|
+ phone: cfg.phone,
|
|
|
|
|
+ password: 'admin123'
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ resp = await apiCall('POST', '/api/admin-auth/login', {
|
|
|
|
|
+ phone: cfg.phone,
|
|
|
|
|
+ code: '123456'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (resp.body && resp.body.code === 200 && resp.body.data && resp.body.data.token) {
|
|
|
|
|
+ tokens[role] = resp.body.data.token;
|
|
|
|
|
+ log('Login ' + role + ' OK');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log('Login ' + role + ' FAILED: ' + (resp.body ? resp.body.message : resp.raw));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// TEST FLOWS
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 1: 认证与用户管理 (user-auth-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testAuthFlow() {
|
|
|
|
|
+ var F = '认证与用户管理';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 家长 ---
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/auth/verify', {}, t);
|
|
|
|
|
+ record(F, 'parent', 'JWT验证', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/user/info', {}, t);
|
|
|
|
|
+ record(F, 'parent', '获取用户信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'userId=' + (r.body.data && r.body.data.id) : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/roles', {}, t);
|
|
|
|
|
+ record(F, 'parent', '获取角色列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'roles=' + JSON.stringify(r.body.data) : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 切换到孩子视图(switch-role需要用户已拥有该角色,家长切孩子视图应使用 member/switch)
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/member/switch', { memberId: 1003 }, t);
|
|
|
|
|
+ record(F, 'parent', '切换到孩子视图', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/switch-back-to-parent', {}, t);
|
|
|
|
|
+ record(F, 'parent', '切换回家长', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 孩子 ---
|
|
|
|
|
+ t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/auth/verify', {}, t);
|
|
|
|
|
+ record(F, 'child', 'JWT验证', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/user/info', {}, t);
|
|
|
|
|
+ record(F, 'child', '获取用户信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'userId=' + (r.body.data && r.body.data.id) : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 教师/规划师 ---
|
|
|
|
|
+ t = tokens.teacher;
|
|
|
|
|
+ if (!t) { skip(F, 'teacher', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/auth/verify', {}, t);
|
|
|
|
|
+ record(F, 'teacher', 'JWT验证', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/user/info', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '获取用户信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'userId=' + (r.body.data && r.body.data.id) : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 管理员 ---
|
|
|
|
|
+ t = tokens.admin;
|
|
|
|
|
+ if (!t) { skip(F, 'admin', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin-auth/info', {}, t);
|
|
|
|
|
+ record(F, 'admin', '管理员信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'admin OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 2: 家庭管理 (family-management-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testFamilyFlow() {
|
|
|
|
|
+ var F = '家庭管理';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 用户与角色管理
|
|
|
|
|
+ var r = await apiCall('POST', '/api/family/user/info', {}, t);
|
|
|
|
|
+ record(F, 'parent', '用户信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'info OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/roles', {}, t);
|
|
|
|
|
+ record(F, 'parent', '角色列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'roles OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 切换到孩子视角(switch-to-child已废弃,使用 /api/family/member/switch)
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/member/switch', { memberId: 1003 }, t);
|
|
|
|
|
+ record(F, 'parent', '切换到孩子视图', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/switch-back-to-parent', {}, t);
|
|
|
|
|
+ record(F, 'parent', '切换回家长', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家庭成员管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/member/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '成员列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'members=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/children/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '孩子列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'children=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/family-members', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家庭成员列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'familyMembers OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 可见成员
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/user/members/visible', {}, t);
|
|
|
|
|
+ record(F, 'parent', '可见成员', r.status === 200 && r.body && r.body.code === 200, r.body ? 'visible OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 邀请码(invite-code已废弃,使用 /api/family/invite/qrcode)
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/invite/qrcode', {}, t);
|
|
|
|
|
+ record(F, 'parent', '查看邀请码', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data && r.body.data.inviteCode ? 'inviteCode=' + r.body.data.inviteCode : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家庭关系问卷
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/questionnaire/generate', {}, t);
|
|
|
|
|
+ record(F, 'parent', 'AI生成问卷', r.status === 200 && r.body && r.body.code === 200, r.body ? 'questionnaire OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 关系质量
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/relationship/scores', {}, t);
|
|
|
|
|
+ record(F, 'parent', '关系质量评分', r.status === 200 && r.body && r.body.code === 200, r.body ? 'scores OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 互动记录
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/interaction/types', {}, t);
|
|
|
|
|
+ record(F, 'parent', '互动类型', r.status === 200 && r.body && r.body.code === 200, r.body ? 'types OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/interaction/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '互动列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'interaction list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 规划师绑定
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/teacher-bindings/pending', {}, t);
|
|
|
|
|
+ record(F, 'parent', '待审批绑定', r.status === 200 && r.body && r.body.code === 200, r.body ? 'pending bindings OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/family/teacher-bindings/my-requests', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的请求', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my requests OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 3: 任务管理 (task-management-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testTaskFlow() {
|
|
|
|
|
+ var F = '任务管理';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 家长:创建任务 ---
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/tasks/create', {
|
|
|
|
|
+ childId: 1003,
|
|
|
|
|
+ title: 'TEST-Flow-ReadBook',
|
|
|
|
|
+ description: 'Reading test flow book',
|
|
|
|
|
+ category: 'study',
|
|
|
|
|
+ points: 10,
|
|
|
|
|
+ requireReview: true
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ var taskId = (r.body && r.body.code === 200 && r.body.data) ? r.body.data.id || r.body.data.taskId : null;
|
|
|
|
|
+ record(F, 'parent', '创建任务', r.status === 200 && r.body && r.body.code === 200, taskId ? 'taskId=' + taskId : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家长今日任务
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/today-parent', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家长今日任务', r.status === 200 && r.body && r.body.code === 200, r.body ? 'today-parent OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 可选小游戏
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/minigame-options', {}, t);
|
|
|
|
|
+ record(F, 'parent', '可选小游戏', r.status === 200 && r.body && r.body.code === 200, r.body ? 'minigame options OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 孩子:执行任务 ---
|
|
|
|
|
+ var t2 = tokens.child;
|
|
|
|
|
+ if (!t2) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/today', {}, t2);
|
|
|
|
|
+ var tasks = (r.body && r.body.code === 200 && r.body.data) ? (Array.isArray(r.body.data) ? r.body.data : (r.body.data.tasks || [])) : [];
|
|
|
|
|
+ record(F, 'child', '今日任务列表', r.status === 200 && r.body && r.body.code === 200, 'count=' + tasks.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 找到刚创建的任务或任意待完成任务
|
|
|
|
|
+ var targetTask = null;
|
|
|
|
|
+ for (var i = 0; i < tasks.length; i++) {
|
|
|
|
|
+ if (tasks[i].status === 'pending' || tasks[i].status === 'active') {
|
|
|
|
|
+ targetTask = tasks[i];
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (targetTask) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/' + targetTask.id + '/complete', { evidence: 'Test complete' }, t2);
|
|
|
|
|
+ record(F, 'child', '完成任务', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ skip(F, 'child', '完成任务', '无可用待完成任务');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务历史
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/history', {}, t2);
|
|
|
|
|
+ record(F, 'child', '任务历史', r.status === 200 && r.body && r.body.code === 200, r.body ? 'history OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 家长:审核 ---
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/pending-review', {}, t);
|
|
|
|
|
+ var pending = (r.body && r.body.code === 200 && r.body.data) ? (Array.isArray(r.body.data) ? r.body.data : (r.body.data.tasks || [])) : [];
|
|
|
|
|
+ record(F, 'parent', '待审核列表', r.status === 200 && r.body && r.body.code === 200, 'pending=' + pending.length);
|
|
|
|
|
+
|
|
|
|
|
+ if (pending.length > 0) {
|
|
|
|
|
+ var reviewTask = pending[0];
|
|
|
|
|
+ var reviewId = reviewTask.id || reviewTask.taskId;
|
|
|
|
|
+ r = await apiCall('POST', '/api/tasks/' + reviewId + '/review', { approved: true }, t);
|
|
|
|
|
+ record(F, 'parent', '审核通过任务', r.status === 200 && r.body && r.body.code === 200, r.body ? 'review approved OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务提醒
|
|
|
|
|
+ r = await apiCall('POST', '/api/task-reminders/upcoming/1003', {}, t);
|
|
|
|
|
+ record(F, 'parent', '即将到期任务', r.status === 200 && r.body && r.body.code === 200, r.body ? 'upcoming OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/task-reminders/overdue/1003', {}, t);
|
|
|
|
|
+ record(F, 'parent', '逾期任务', r.status === 200 && r.body && r.body.code === 200, r.body ? 'overdue OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/task-reminders/statistics/1003', {}, t);
|
|
|
|
|
+ record(F, 'parent', '任务统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'stats OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 清理:删除测试任务
|
|
|
|
|
+ if (taskId) {
|
|
|
|
|
+ await apiCall('POST', '/api/tasks/' + taskId + '/delete', {}, t);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 4: 心愿管理 (wish-management-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testWishFlow() {
|
|
|
|
|
+ var F = '心愿管理';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 孩子:创建心愿 ---
|
|
|
|
|
+ var t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/wishes/create', {
|
|
|
|
|
+ title: 'TEST-Flow-NewBook',
|
|
|
|
|
+ description: 'A new storybook',
|
|
|
|
|
+ category: 'education'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ var wishId = (r.body && r.body.code === 200 && r.body.data) ? (r.body.data.id || r.body.data.wishId) : null;
|
|
|
|
|
+ record(F, 'child', '创建心愿', r.status === 200 && r.body && r.body.code === 200, wishId ? 'wishId=' + wishId : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 家长:审批 ---
|
|
|
|
|
+ var t2 = tokens.parent;
|
|
|
|
|
+ if (!t2) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/pending', { familyId: 2001 }, t2);
|
|
|
|
|
+ record(F, 'parent', '待处理心愿', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'pending=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ if (wishId) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId + '/set-price', { price: 50 }, t2);
|
|
|
|
|
+ record(F, 'parent', '家长定价', r.status === 200 && r.body && r.body.code === 200, r.body ? 'price set OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 孩子申请兑换
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId + '/exchange', {}, t);
|
|
|
|
|
+ record(F, 'child', '申请兑换', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家长审批兑换
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId + '/approve-exchange', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '审批兑换', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家长标记已购买
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId + '/fulfill', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '标记已购买', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 孩子确认收货
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId + '/confirm', {}, t);
|
|
|
|
|
+ record(F, 'child', '确认收货', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 心愿列表
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '心愿列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'wishes=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 心愿详情
|
|
|
|
|
+ if (wishId) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/wishes/' + wishId, {}, t);
|
|
|
|
|
+ record(F, 'child', '心愿详情', r.status === 200 && r.body && r.body.code === 200, r.body ? 'detail OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 5: 五维能量系统 (energy-system-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testEnergyFlow() {
|
|
|
|
|
+ var F = '五维能量';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/energy/overview', {}, t);
|
|
|
|
|
+ record(F, 'parent', '五维概览', r.status === 200 && r.body && r.body.code === 200, r.body ? 'overview OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/sandbox', {}, t);
|
|
|
|
|
+ record(F, 'parent', '能量沙盘', r.status === 200 && r.body && r.body.code === 200, r.body ? 'sandbox OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/logs', {}, t);
|
|
|
|
|
+ record(F, 'parent', '能量流水', r.status === 200 && r.body && r.body.code === 200, r.body ? 'logs OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/score-history', {}, t);
|
|
|
|
|
+ record(F, 'parent', '分数历史', r.status === 200 && r.body && r.body.code === 200, r.body ? 'history OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/family-score-history', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家庭五维聚合', r.status === 200 && r.body && r.body.code === 200, r.body ? 'family score OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 能量发放
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/grant', { childId: 1003, amount: 5, reason: 'Test grant' }, t);
|
|
|
|
|
+ record(F, 'parent', '发放能量', r.status === 200 && r.body && r.body.code === 200, r.body ? 'grant OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 孩子维度
|
|
|
|
|
+ var t2 = tokens.child;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/energy/overview', {}, t2);
|
|
|
|
|
+ record(F, 'child', '五维概览', r.status === 200 && r.body && r.body.code === 200, r.body ? 'child overview OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 维度管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/dimension/overview', {}, t);
|
|
|
|
|
+ record(F, 'parent', '维度概览', r.status === 200 && r.body && r.body.code === 200, r.body ? 'dimension overview OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 年度能量
|
|
|
|
|
+ r = await apiCall('POST', '/api/zodiac/energy', {}, t);
|
|
|
|
|
+ record(F, 'parent', '年度五维能量', r.status === 200 && r.body && r.body.code === 200, r.body ? 'zodiac energy OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家庭收入
|
|
|
|
|
+ r = await apiCall('POST', '/api/earnings/family/summary', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家庭收入汇总', r.status === 200 && r.body && r.body.code === 200, r.body ? 'earnings summary OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 6: 健康打卡 (health-checkin-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testCheckinFlow() {
|
|
|
|
|
+ var F = '健康打卡';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 综合打卡
|
|
|
|
|
+ var r = await apiCall('POST', '/api/daily/checkin/create', {
|
|
|
|
|
+ type: 'comprehensive',
|
|
|
|
|
+ mood: 'happy',
|
|
|
|
|
+ energy: 8,
|
|
|
|
|
+ note: 'Test checkin'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '综合打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 运动打卡
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/exercise/create', {
|
|
|
|
|
+ exerciseType: 'running',
|
|
|
|
|
+ duration: 30,
|
|
|
|
|
+ note: 'Test run'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '运动打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'exercise OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 饮食打卡
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/meal/create', {
|
|
|
|
|
+ mealType: 'lunch',
|
|
|
|
|
+ foods: 'rice,vegetables',
|
|
|
|
|
+ note: 'Test meal'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '饮食打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'meal OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 睡眠打卡
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/sleep/create', {
|
|
|
|
|
+ sleepTime: '22:00',
|
|
|
|
|
+ wakeTime: '07:00',
|
|
|
|
|
+ quality: 'good'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '睡眠打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'sleep OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 喝水打卡
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/water/create', {
|
|
|
|
|
+ amount: 2000,
|
|
|
|
|
+ note: 'Drank water'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '喝水打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'water OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 打卡列表(需要memberId参数)
|
|
|
|
|
+ r = await apiCall('POST', '/api/daily/checkin/list', { memberId: 1002 }, t);
|
|
|
|
|
+ record(F, 'child', '打卡列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 打卡统计
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/checkin/stats', {}, t);
|
|
|
|
|
+ record(F, 'child', '打卡统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'stats OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 打卡进度
|
|
|
|
|
+ r = await apiCall('POST', '/api/streak/progress/1003', {}, t);
|
|
|
|
|
+ record(F, 'child', '打卡进度', r.status === 200 && r.body && r.body.code === 200, r.body ? 'streak OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 健康积分
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/score/info', {}, t);
|
|
|
|
|
+ record(F, 'child', '健康积分', r.status === 200 && r.body && r.body.code === 200, r.body ? 'health score OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 健康时间线
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/timeline/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '健康时间线', r.status === 200 && r.body && r.body.code === 200, r.body ? 'timeline OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 健康预警
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/alert/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '健康预警', r.status === 200 && r.body && r.body.code === 200, r.body ? 'alerts OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 7: 成长档案 (growth-record-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testGrowthFlow() {
|
|
|
|
|
+ var F = '成长档案';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建成长档案
|
|
|
|
|
+ var r = await apiCall('POST', '/api/growth/record/create', {
|
|
|
|
|
+ childId: 1003,
|
|
|
|
|
+ title: 'Test Growth Record',
|
|
|
|
|
+ type: 'assessment',
|
|
|
|
|
+ description: 'Test growth record creation'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ var recordId = (r.body && r.body.code === 200 && r.body.data) ? (r.body.data.id || r.body.data.recordId) : null;
|
|
|
|
|
+ record(F, 'parent', '创建成长档案', r.status === 200 && r.body && r.body.code === 200, recordId ? 'recordId=' + recordId : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 档案列表
|
|
|
|
|
+ r = await apiCall('POST', '/api/growth/record/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '档案列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'records=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 指定孩子档案
|
|
|
|
|
+ r = await apiCall('POST', '/api/growth/record/child/1003', {}, t);
|
|
|
|
|
+ record(F, 'parent', '指定孩子档案', r.status === 200 && r.body && r.body.code === 200, r.body ? 'child records OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 档案详情
|
|
|
|
|
+ if (recordId) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/growth/record/' + recordId, {}, t);
|
|
|
|
|
+ record(F, 'parent', '档案详情', r.status === 200 && r.body && r.body.code === 200, r.body ? 'detail OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生长记录
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/growth/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '生长记录列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'growth list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/growth/create', {
|
|
|
|
|
+ childId: 1003,
|
|
|
|
|
+ height: 135.5,
|
|
|
|
|
+ weight: 30.2,
|
|
|
|
|
+ recordedDate: '2026-07-20'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'parent', '创建生长记录', r.status === 200 && r.body && r.body.code === 200, r.body ? 'growth create OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 成长计划
|
|
|
|
|
+ r = await apiCall('POST', '/api/growth/plan/create', {
|
|
|
|
|
+ childId: 1003,
|
|
|
|
|
+ title: 'Test Growth Plan',
|
|
|
|
|
+ goals: 'Improve reading skills'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'parent', '创建成长计划', r.status === 200 && r.body && r.body.code === 200, r.body ? 'plan create OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/growth/plan/child/1003', {}, t);
|
|
|
|
|
+ record(F, 'parent', '查看孩子计划', r.status === 200 && r.body && r.body.code === 200, r.body ? 'child plans OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 清理
|
|
|
|
|
+ if (recordId) {
|
|
|
|
|
+ await apiCall('POST', '/api/growth/record/' + recordId + '/delete', {}, t);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 8: DAN测评 (dan-assessment-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testDanFlow() {
|
|
|
|
|
+ var F = 'DAN测评';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 测评准备
|
|
|
|
|
+ var r = await apiCall('POST', '/api/dan-assessment/material/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '测评材料列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'materials OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/dan-assessment/material/active', {}, t);
|
|
|
|
|
+ record(F, 'parent', '启用材料', r.status === 200 && r.body && r.body.code === 200, r.body ? 'active material OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/dan-assessment/family/config', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家庭测评配置', r.status === 200 && r.body && r.body.code === 200, r.body ? 'family config OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 额度
|
|
|
|
|
+ r = await apiCall('POST', '/api/assessment/quota/my-list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '可用额度', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'quota list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建测评订单
|
|
|
|
|
+ r = await apiCall('POST', '/api/dan-assessment/order/create', {
|
|
|
|
|
+ childId: 1003,
|
|
|
|
|
+ materialId: 1
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'parent', '创建测评订单', r.status === 200 && r.body && r.body.code === 200, r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 评估师列表
|
|
|
|
|
+ r = await apiCall('POST', '/api/assessment/appointment/assessor-list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '评估师列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'assessors=' + (Array.isArray(r.body.data) ? r.body.data.length : 'N/A') : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 最新测评结果
|
|
|
|
|
+ r = await apiCall('POST', '/api/assessment/latest-result', {}, t);
|
|
|
|
|
+ record(F, 'parent', '最新测评结果', r.status === 200 && r.body && r.body.code === 200, r.body ? 'latest result OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 测评历史
|
|
|
|
|
+ r = await apiCall('POST', '/api/assessment/history', {}, t);
|
|
|
|
|
+ record(F, 'parent', '测评历史', r.status === 200 && r.body && r.body.code === 200, r.body ? 'history OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 我的报告
|
|
|
|
|
+ r = await apiCall('POST', '/api/assessment/my-results', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的报告', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my results OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家长自评
|
|
|
|
|
+ r = await apiCall('POST', '/api/parent/assessment/create', {}, t);
|
|
|
|
|
+ record(F, 'parent', '创建家长自评', r.status === 200 && r.body && r.body.code === 200, r.body ? 'self assessment OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/parent/assessment/records', {}, t);
|
|
|
|
|
+ record(F, 'parent', '自评记录', r.status === 200 && r.body && r.body.code === 200, r.body ? 'assessment records OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 规划师端 ---
|
|
|
|
|
+ var t2 = tokens.teacher;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/assessment/families', {}, t2);
|
|
|
|
|
+ record(F, 'teacher', '家庭测评列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'guide assessment families OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 9: 商城交易 (shop-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testShopFlow() {
|
|
|
|
|
+ var F = '商城交易';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 商品浏览
|
|
|
|
|
+ var r = await apiCall('POST', '/api/shop/category/tree', {}, t);
|
|
|
|
|
+ record(F, 'parent', '商品分类树', r.status === 200 && r.body && r.body.code === 200, r.body ? 'category tree OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/product/list', {}, t);
|
|
|
|
|
+ var products = (r.body && r.body.code === 200 && r.body.data) ? (Array.isArray(r.body.data) ? r.body.data : (r.body.data.records || [])) : [];
|
|
|
|
|
+ record(F, 'parent', '商品列表', r.status === 200 && r.body && r.body.code === 200, 'products=' + products.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 购物车
|
|
|
|
|
+ r = await apiCall('POST', '/api/cart/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '购物车列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'cart list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/cart/count', {}, t);
|
|
|
|
|
+ record(F, 'parent', '购物车数量', r.status === 200 && r.body && r.body.code === 200, r.body ? 'cart count OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 我的订单
|
|
|
|
|
+ r = await apiCall('POST', '/api/product/order/my', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的订单', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my orders OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/product/order/my/counts', {}, t);
|
|
|
|
|
+ record(F, 'parent', '订单数量统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'order counts OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 收货地址
|
|
|
|
|
+ r = await apiCall('POST', '/api/consignee/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '收货地址列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'consignee list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 售后
|
|
|
|
|
+ r = await apiCall('POST', '/api/shop/after-sales/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '售后列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'after-sales OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 管理员 ---
|
|
|
|
|
+ var t2 = tokens.admin;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/product/my', {}, t2);
|
|
|
|
|
+ record(F, 'admin', '管理端商品列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'admin products OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 10: 积分兑换 (points-exchange-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testPointsFlow() {
|
|
|
|
|
+ var F = '积分兑换';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/points/balance', {}, t);
|
|
|
|
|
+ record(F, 'child', '积分余额', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'balance OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/points/logs', {}, t);
|
|
|
|
|
+ record(F, 'child', '积分流水', r.status === 200 && r.body && r.body.code === 200, r.body ? 'logs OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 兑换商品列表
|
|
|
|
|
+ r = await apiCall('POST', '/api/points/exchange/product/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '兑换商品列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'exchange products OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 奖励系统
|
|
|
|
|
+ r = await apiCall('POST', '/api/rewards/wishlist', {}, t);
|
|
|
|
|
+ record(F, 'child', '奖励清单', r.status === 200 && r.body && r.body.code === 200, r.body ? 'wishlist OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/rewards/templates', {}, t);
|
|
|
|
|
+ record(F, 'child', '奖励模板', r.status === 200 && r.body && r.body.code === 200, r.body ? 'templates OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家长端积分
|
|
|
|
|
+ var t2 = tokens.parent;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/points/balance', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '家长积分余额', r.status === 200 && r.body && r.body.code === 200, r.body ? 'parent balance OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/points/exchange/records', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '兑换记录', r.status === 200 && r.body && r.body.code === 200, r.body ? 'exchange records OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 11: 小游戏 (minigame-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testMinigameFlow() {
|
|
|
|
|
+ var F = '小游戏';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 游戏列表
|
|
|
|
|
+ var r = await apiCall('POST', '/api/mini-game/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '小游戏列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'games OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 游戏详情 (Schulte Table)
|
|
|
|
|
+ r = await apiCall('POST', '/api/mini-game/schulte', {}, t);
|
|
|
|
|
+ record(F, 'child', '舒尔特方格详情', r.status === 200 && r.body && r.body.code === 200, r.body ? 'schulte OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 完成游戏
|
|
|
|
|
+ r = await apiCall('POST', '/api/mini-game/complete', {
|
|
|
|
|
+ gameCode: 'schulte',
|
|
|
|
|
+ score: 95,
|
|
|
|
|
+ timeSpent: 30
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '完成游戏', r.status === 200 && r.body && r.body.code === 200, r.body ? 'complete OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 游戏历史
|
|
|
|
|
+ r = await apiCall('POST', '/api/game/history', {}, t);
|
|
|
|
|
+ record(F, 'child', '游戏历史', r.status === 200 && r.body && r.body.code === 200, r.body ? 'history OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 最佳成绩
|
|
|
|
|
+ r = await apiCall('POST', '/api/game/best', {}, t);
|
|
|
|
|
+ record(F, 'child', '最佳成绩', r.status === 200 && r.body && r.body.code === 200, r.body ? 'best OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 排行榜
|
|
|
|
|
+ r = await apiCall('POST', '/api/game/leaderboard', {}, t);
|
|
|
|
|
+ record(F, 'child', '排行榜', r.status === 200 && r.body && r.body.code === 200, r.body ? 'leaderboard OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 智慧数学
|
|
|
|
|
+ r = await apiCall('POST', '/api/wisdom/math/start', {}, t);
|
|
|
|
|
+ record(F, 'child', '开始数学游戏', r.status === 200 && r.body && r.body.code === 200, r.body ? 'math start OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 12: 营养与饮食 (nutrition-diet-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testNutritionFlow() {
|
|
|
|
|
+ var F = '营养与饮食';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 食谱推荐
|
|
|
|
|
+ var r = await apiCall('POST', '/api/meal/recommend', {}, t);
|
|
|
|
|
+ record(F, 'parent', '食谱推荐', r.status === 200 && r.body && r.body.code === 200, r.body ? 'meal recommend OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 饮食日志
|
|
|
|
|
+ r = await apiCall('POST', '/api/meal/logs', {}, t);
|
|
|
|
|
+ record(F, 'parent', '饮食日志', r.status === 200 && r.body && r.body.code === 200, r.body ? 'meal logs OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 营养摄入统计
|
|
|
|
|
+ r = await apiCall('POST', '/api/meal/nutrition-summary', {}, t);
|
|
|
|
|
+ record(F, 'parent', '营养摄入统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'nutrition summary OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 食材推荐指数
|
|
|
|
|
+ r = await apiCall('POST', '/api/food/recommendation/index', {}, t);
|
|
|
|
|
+ record(F, 'parent', '食材推荐指数', r.status === 200 && r.body && r.body.code === 200, r.body ? 'food index OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 营养偏好
|
|
|
|
|
+ r = await apiCall('POST', '/api/nutrition/profile/get', {}, t);
|
|
|
|
|
+ record(F, 'parent', '获取营养偏好', r.status === 200 && r.body && r.body.code === 200, r.body ? 'nutrition profile OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 维度推荐
|
|
|
|
|
+ r = await apiCall('POST', '/api/recommend/dimension-products', {}, t);
|
|
|
|
|
+ record(F, 'parent', '维度推荐商品', r.status === 200 && r.body && r.body.code === 200, r.body ? 'dimension products OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 北京知识库
|
|
|
|
|
+ r = await apiCall('POST', '/api/nutrition/beijing/kb/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '营养知识库', r.status === 200 && r.body && r.body.code === 200, r.body ? 'kb list OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 13: 心理与情绪 (mind-emotion-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testMindFlow() {
|
|
|
|
|
+ var F = '心理与情绪';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.child;
|
|
|
|
|
+ if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 情绪打卡
|
|
|
|
|
+ var r = await apiCall('POST', '/api/mind/checkin/create', {
|
|
|
|
|
+ emotion: 'happy',
|
|
|
|
|
+ intensity: 8,
|
|
|
|
|
+ note: 'Feeling good today'
|
|
|
|
|
+ }, t);
|
|
|
|
|
+ record(F, 'child', '情绪打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'emotion checkin OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/checkin/list', {}, t);
|
|
|
|
|
+ record(F, 'child', '情绪列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'emotion list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/checkin/latest', {}, t);
|
|
|
|
|
+ record(F, 'child', '最新情绪', r.status === 200 && r.body && r.body.code === 200, r.body ? 'latest OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/checkin/trend', {}, t);
|
|
|
|
|
+ record(F, 'child', '情绪趋势', r.status === 200 && r.body && r.body.code === 200, r.body ? 'trend OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/checkin/stats', {}, t);
|
|
|
|
|
+ record(F, 'child', '情绪统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'stats OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 心理测评
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/screening/history', {}, t);
|
|
|
|
|
+ record(F, 'child', '筛查历史', r.status === 200 && r.body && r.body.code === 200, r.body ? 'screening history OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 心理运势
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/fortune', {}, t);
|
|
|
|
|
+ record(F, 'child', '心理运势', r.status === 200 && r.body && r.body.code === 200, r.body ? 'fortune OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // EMI报告
|
|
|
|
|
+ r = await apiCall('POST', '/api/mind/emireport/latest', {}, t);
|
|
|
|
|
+ record(F, 'child', 'EMI报告', r.status === 200 && r.body && r.body.code === 200, r.body ? 'EMI report OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 14: 身体健康检测 (health-detection-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testHealthDetectionFlow() {
|
|
|
|
|
+ var F = '身体健康检测';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/health/report/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '健康报告列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'report list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/report/latest', {}, t);
|
|
|
|
|
+ record(F, 'parent', '最新报告', r.status === 200 && r.body && r.body.code === 200, r.body ? 'latest report OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/indicator/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '指标明细', r.status === 200 && r.body && r.body.code === 200, r.body ? 'indicators OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/nutrition/deficiency', {}, t);
|
|
|
|
|
+ record(F, 'parent', '营养缺乏分析', r.status === 200 && r.body && r.body.code === 200, r.body ? 'deficiency OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 15: 活动管理 (activity-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testActivityFlow() {
|
|
|
|
|
+ var F = '活动管理';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 活动列表
|
|
|
|
|
+ var r = await apiCall('POST', '/api/activity/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '活动列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'activities OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 我的报名
|
|
|
|
|
+ r = await apiCall('POST', '/api/activity/my-registrations', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的报名', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my registrations OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 管理员 ---
|
|
|
|
|
+ var t2 = tokens.admin;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/activity/list', {}, t2);
|
|
|
|
|
+ record(F, 'admin', '管理端活动列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'admin activities OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 16: 内容与媒体 (content-media-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testContentFlow() {
|
|
|
|
|
+ var F = '内容与媒体';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/articles/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '文章列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'articles OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/articles/categories', {}, t);
|
|
|
|
|
+ record(F, 'parent', '文章分类', r.status === 200 && r.body && r.body.code === 200, r.body ? 'categories OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/articles/featured', {}, t);
|
|
|
|
|
+ record(F, 'parent', '精选文章', r.status === 200 && r.body && r.body.code === 200, r.body ? 'featured OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/articles/daily-tip', {}, t);
|
|
|
|
|
+ record(F, 'parent', '每日贴士', r.status === 200 && r.body && r.body.code === 200, r.body ? 'daily tip OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 分享
|
|
|
|
|
+ r = await apiCall('POST', '/api/share/my', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的分享', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my shares OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 媒体
|
|
|
|
|
+ r = await apiCall('POST', '/api/media/upload-text', { text: 'Test media text' }, t);
|
|
|
|
|
+ record(F, 'parent', '保存文字记录', r.status === 200 && r.body && r.body.code === 200, r.body ? 'text upload OK' : r.body ? r.body.message : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 17: AI助手 (ai-assistant-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testAIFlow() {
|
|
|
|
|
+ var F = 'AI助手';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 会话列表
|
|
|
|
|
+ var r = await apiCall('POST', '/api/ai/chat/conversations', {}, t);
|
|
|
|
|
+ record(F, 'parent', 'AI会话列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'conversations OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // AI管家会话
|
|
|
|
|
+ r = await apiCall('POST', '/api/ai/butler/sessions/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', 'AI管家会话列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'butler sessions OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // AI上下文
|
|
|
|
|
+ r = await apiCall('POST', '/api/ai/context', {}, t);
|
|
|
|
|
+ record(F, 'parent', 'AI上下文', r.status === 200 && r.body && r.body.code === 200, r.body ? 'context OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 推荐搜索
|
|
|
|
|
+ r = await apiCall('POST', '/api/recommend/search', { keyword: 'nutrition' }, t);
|
|
|
|
|
+ record(F, 'parent', '推荐搜索', r.status === 200 && r.body && r.body.code === 200, r.body ? 'recommend search OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 18: 会员与订阅 (membership-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testMembershipFlow() {
|
|
|
|
|
+ var F = '会员与订阅';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/membership/levels', {}, t);
|
|
|
|
|
+ record(F, 'parent', '会员等级列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'levels OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/membership/my', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的会员信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my membership OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/membership/level', {}, t);
|
|
|
|
|
+ record(F, 'parent', '当前等级', r.status === 200 && r.body && r.body.code === 200, r.body ? 'current level OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/membership/can-use', {}, t);
|
|
|
|
|
+ record(F, 'parent', '功能权限', r.status === 200 && r.body && r.body.code === 200, r.body ? 'can-use OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/membership/discount', {}, t);
|
|
|
|
|
+ record(F, 'parent', '会员折扣', r.status === 200 && r.body && r.body.code === 200, r.body ? 'discount OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 订阅
|
|
|
|
|
+ r = await apiCall('POST', '/api/subscription/status', {}, t);
|
|
|
|
|
+ record(F, 'parent', '订阅状态', r.status === 200 && r.body && r.body.code === 200, r.body ? 'subscription status OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/subscription/plans', {}, t);
|
|
|
|
|
+ record(F, 'parent', '订阅计划', r.status === 200 && r.body && r.body.code === 200, r.body ? 'subscription plans OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 19: 管理后台 (admin-overview-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testAdminFlow() {
|
|
|
|
|
+ var F = '管理后台';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.admin;
|
|
|
|
|
+ if (!t) { skip(F, 'admin', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 用户管理
|
|
|
|
|
+ var r = await apiCall('POST', '/api/admin/users', {}, t);
|
|
|
|
|
+ record(F, 'admin', '用户列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'users OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 孩子管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/children', {}, t);
|
|
|
|
|
+ record(F, 'admin', '孩子列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'children OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家庭管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/families', {}, t);
|
|
|
|
|
+ record(F, 'admin', '家庭列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'families OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 任务管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/tasks', {}, t);
|
|
|
|
|
+ record(F, 'admin', '任务列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'tasks OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 积分管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/points/logs', {}, t);
|
|
|
|
|
+ record(F, 'admin', '积分日志', r.status === 200 && r.body && r.body.code === 200, r.body ? 'points logs OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 规划师审核
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/guide/applications/pending', {}, t);
|
|
|
|
|
+ record(F, 'admin', '规划师待审批', r.status === 200 && r.body && r.body.code === 200, r.body ? 'guide pending OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 商品管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/product/list', {}, t);
|
|
|
|
|
+ record(F, 'admin', '商品列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'product list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 订单管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/admin/product/order/list', {}, t);
|
|
|
|
|
+ record(F, 'admin', '订单列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'order list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 小游戏管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/mini-game/all', {}, t);
|
|
|
|
|
+ record(F, 'admin', '所有游戏', r.status === 200 && r.body && r.body.code === 200, r.body ? 'all games OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 健康检查
|
|
|
|
|
+ r = await apiCall('POST', '/api/system/health', {}, t);
|
|
|
|
|
+ record(F, 'admin', '系统健康检查', r.status === 200 && r.body && r.body.code === 200, r.body ? 'health OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 20: 规划师体系 (guide-system-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testGuideFlow() {
|
|
|
|
|
+ var F = '规划师体系';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.teacher;
|
|
|
|
|
+ if (!t) { skip(F, 'teacher', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 个人信息
|
|
|
|
|
+ var r = await apiCall('POST', '/api/guide/my', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '我的信息', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my info OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/my/packages', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '我的套餐', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my packages OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 团队管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/team/my-team', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '我的团队', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my team OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/team/members', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '团队成员', r.status === 200 && r.body && r.body.code === 200, r.body ? 'team members OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 绑定家庭
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/families/bound-families', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '绑定家庭', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'bound families OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 任务模板
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/packages/list', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '模板列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'package list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 训练方案
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/training-plans/list', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '方案列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'training plans OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 活动管理
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/activities/list', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '活动列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'activities OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 成长记录
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/record/my-records', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '我的记录', r.status === 200 && r.body && r.body.code === 200, r.body ? 'records OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 咨询消息
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/messages/list', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '咨询列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'messages OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 订单佣金
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/orders/list', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '订单列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'orders OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 仪表盘
|
|
|
|
|
+ r = await apiCall('POST', '/api/guide/families/dashboard-stats', {}, t);
|
|
|
|
|
+ record(F, 'teacher', '仪表盘统计', r.status === 200 && r.body && r.body.code === 200, r.body ? 'dashboard stats OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 21: 圈子与联系人 (circle-contact-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testCircleFlow() {
|
|
|
|
|
+ var F = '圈子与联系人';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ var r = await apiCall('POST', '/api/circle/my-circles', {}, t);
|
|
|
|
|
+ record(F, 'parent', '我的圈子', r.status === 200 && r.body && r.body.code === 200, r.body ? 'my circles OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/circle/discover', {}, t);
|
|
|
|
|
+ record(F, 'parent', '发现圈子', r.status === 200 && r.body && r.body.code === 200, r.body ? 'discover OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 健康圈子
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/circle/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '健康圈子列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'health circle OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 联系人
|
|
|
|
|
+ r = await apiCall('POST', '/api/contact/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '联系人列表', r.status === 200 && r.body && r.body.code === 200, r.body && r.body.data ? 'contacts OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 家庭挑战
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/challenge/list', {}, t);
|
|
|
|
|
+ record(F, 'parent', '家庭挑战列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'challenges OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 22: 知识库与邀请 (knowledge-invite-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testKnowledgeFlow() {
|
|
|
|
|
+ var F = '知识库与邀请';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ // 管理员:知识库
|
|
|
|
|
+ var t = tokens.admin;
|
|
|
|
|
+ if (t) {
|
|
|
|
|
+ var r = await apiCall('POST', '/api/health/knowledge/list', {}, t);
|
|
|
|
|
+ record(F, 'admin', '知识库列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'kb list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/knowledge/v3/bacteria/query', { keyword: 'lactobacillus' }, t);
|
|
|
|
|
+ record(F, 'admin', '菌属查询', r.status === 200 && r.body && r.body.code === 200, r.body ? 'bacteria query OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/health/knowledge/v3/food/query', { keyword: 'apple' }, t);
|
|
|
|
|
+ record(F, 'admin', '食物营养查询', r.status === 200 && r.body && r.body.code === 200, r.body ? 'food query OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 用户:邀请码
|
|
|
|
|
+ var t2 = tokens.parent;
|
|
|
|
|
+ if (t2) {
|
|
|
|
|
+ var r = await apiCall('POST', '/api/invite/code', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '生成邀请码', r.status === 200 && r.body && r.body.code === 200, r.body ? 'invite code OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/invite/list', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '邀请列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'invite list OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/invite/summary', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '邀请汇总', r.status === 200 && r.body && r.body.code === 200, r.body ? 'invite summary OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 里程碑
|
|
|
|
|
+ r = await apiCall('POST', '/api/invite/milestone/list', {}, t2);
|
|
|
|
|
+ record(F, 'parent', '里程碑列表', r.status === 200 && r.body && r.body.code === 200, r.body ? 'milestones OK' : r.raw);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// FLOW 23: 其他功能 (extra-features-flow)
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function testExtraFlow() {
|
|
|
|
|
+ var F = '其他功能';
|
|
|
|
|
+ log('\n========== ' + F + ' ==========');
|
|
|
|
|
+
|
|
|
|
|
+ var t = tokens.parent;
|
|
|
|
|
+ if (!t) { skip(F, 'parent', '所有测试', '未登录'); return; }
|
|
|
|
|
+
|
|
|
|
|
+ // 吉祥物
|
|
|
|
|
+ var r = await apiCall('POST', '/api/user/mascot/get', {}, t);
|
|
|
|
|
+ record(F, 'parent', '获取吉祥物', r.status === 200 && r.body && r.body.code === 200, r.body ? 'mascot OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 每日反馈
|
|
|
|
|
+ r = await apiCall('POST', '/api/feedback/today', {}, t);
|
|
|
|
|
+ record(F, 'parent', '今日反馈', r.status === 200 && r.body && r.body.code === 200, r.body ? 'today feedback OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ r = await apiCall('POST', '/api/feedback/recent', {}, t);
|
|
|
|
|
+ record(F, 'parent', '最近反馈', r.status === 200 && r.body && r.body.code === 200, r.body ? 'recent feedback OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成小程序码
|
|
|
|
|
+ r = await apiCall('POST', '/api/common/qrcode', {}, t);
|
|
|
|
|
+ record(F, 'parent', '生成小程序码', r.status === 200 && r.body && r.body.code === 200, r.body ? 'qrcode OK' : r.raw);
|
|
|
|
|
+
|
|
|
|
|
+ // 引导进度
|
|
|
|
|
+ r = await apiCall('POST', '/api/onboarding/progress', {}, t);
|
|
|
|
|
+ record(F, 'parent', '引导进度', r.status === 200 && r.body && r.body.code === 200, r.body ? 'onboarding OK' : r.raw);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+// MAIN RUNNER
|
|
|
|
|
+// ================================================================
|
|
|
|
|
+
|
|
|
|
|
+async function main() {
|
|
|
|
|
+ console.log('============================================================');
|
|
|
|
|
+ console.log(' 流程级一级测试用例 — FLOW L1 TEST SUITE');
|
|
|
|
|
+ console.log(' Target: ' + BASE_URL);
|
|
|
|
|
+ console.log(' Date: ' + new Date().toISOString());
|
|
|
|
|
+ console.log('============================================================\n');
|
|
|
|
|
+
|
|
|
|
|
+ // Step 1: Login all roles
|
|
|
|
|
+ console.log('--- LOGIN ALL ROLES ---');
|
|
|
|
|
+ await loginAll();
|
|
|
|
|
+ console.log('');
|
|
|
|
|
+
|
|
|
|
|
+ // Step 2: Run all flow tests
|
|
|
|
|
+ await testAuthFlow();
|
|
|
|
|
+ await testFamilyFlow();
|
|
|
|
|
+ await testTaskFlow();
|
|
|
|
|
+ await testWishFlow();
|
|
|
|
|
+ await testEnergyFlow();
|
|
|
|
|
+ await testCheckinFlow();
|
|
|
|
|
+ await testGrowthFlow();
|
|
|
|
|
+ await testDanFlow();
|
|
|
|
|
+ await testShopFlow();
|
|
|
|
|
+ await testPointsFlow();
|
|
|
|
|
+ await testMinigameFlow();
|
|
|
|
|
+ await testNutritionFlow();
|
|
|
|
|
+ await testMindFlow();
|
|
|
|
|
+ await testHealthDetectionFlow();
|
|
|
|
|
+ await testActivityFlow();
|
|
|
|
|
+ await testContentFlow();
|
|
|
|
|
+ await testAIFlow();
|
|
|
|
|
+ await testMembershipFlow();
|
|
|
|
|
+ await testAdminFlow();
|
|
|
|
|
+ await testGuideFlow();
|
|
|
|
|
+ await testCircleFlow();
|
|
|
|
|
+ await testKnowledgeFlow();
|
|
|
|
|
+ await testExtraFlow();
|
|
|
|
|
+
|
|
|
|
|
+ // Step 3: Summary
|
|
|
|
|
+ console.log('\n============================================================');
|
|
|
|
|
+ console.log(' TEST RESULTS SUMMARY');
|
|
|
|
|
+ console.log('============================================================');
|
|
|
|
|
+ console.log(' ✅ Passed: ' + results.passed);
|
|
|
|
|
+ console.log(' ❌ Failed: ' + results.failed);
|
|
|
|
|
+ console.log(' ⏭️ Skipped: ' + results.skipped);
|
|
|
|
|
+ console.log(' Total: ' + (results.passed + results.failed + results.skipped));
|
|
|
|
|
+ console.log('============================================================');
|
|
|
|
|
+
|
|
|
|
|
+ // Group failures by flow
|
|
|
|
|
+ var failures = results.details.filter(function(d) { return d.passed === false; });
|
|
|
|
|
+ if (failures.length > 0) {
|
|
|
|
|
+ console.log('\n--- FAILURES ---');
|
|
|
|
|
+ var byFlow = {};
|
|
|
|
|
+ failures.forEach(function(f) {
|
|
|
|
|
+ if (!byFlow[f.flow]) byFlow[f.flow] = [];
|
|
|
|
|
+ byFlow[f.flow].push(f);
|
|
|
|
|
+ });
|
|
|
|
|
+ for (var flow in byFlow) {
|
|
|
|
|
+ console.log('\n[' + flow + ']');
|
|
|
|
|
+ byFlow[flow].forEach(function(f) {
|
|
|
|
|
+ console.log(' ❌ ' + f.role + ' | ' + f.step + ' — ' + (f.detail || ''));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Group issues
|
|
|
|
|
+ if (results.issues.length > 0) {
|
|
|
|
|
+ console.log('\n--- ISSUES ---');
|
|
|
|
|
+ results.issues.forEach(function(iss) {
|
|
|
|
|
+ console.log('⚠️ [' + iss.flow + '] ' + iss.role + ' | ' + iss.step + ': ' + iss.description);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Flows with all passes
|
|
|
|
|
+ var allFlows = {};
|
|
|
|
|
+ results.details.forEach(function(d) {
|
|
|
|
|
+ if (!allFlows[d.flow]) allFlows[d.flow] = { pass: 0, fail: 0, skip: 0 };
|
|
|
|
|
+ if (d.passed === true) allFlows[d.flow].pass++;
|
|
|
|
|
+ else if (d.passed === false) allFlows[d.flow].fail++;
|
|
|
|
|
+ else allFlows[d.flow].skip++;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('\n--- FLOW STATUS ---');
|
|
|
|
|
+ for (var f in allFlows) {
|
|
|
|
|
+ var s = allFlows[f];
|
|
|
|
|
+ var status = s.fail === 0 ? '✅' : '❌';
|
|
|
|
|
+ console.log(status + ' ' + f + ': ' + s.pass + '/' + (s.pass + s.fail) + ' passed, ' + s.skip + ' skipped');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('\n============================================================');
|
|
|
|
|
+ console.log(' DONE');
|
|
|
|
|
+ console.log('============================================================');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+main().catch(function(err) {
|
|
|
|
|
+ console.error('FATAL:', err);
|
|
|
|
|
+ process.exit(1);
|
|
|
|
|
+});
|