203 行
13 KiB
JavaScript
203 行
13 KiB
JavaScript
/**
|
||
* 逐页面全链路验证(从登录页开始)
|
||
* 按每个页面真实的接口调用顺序回放,验证:页面数据绑定是否拿得到、按钮状态是否正确
|
||
*
|
||
* 运行:node tests/page_flow.js
|
||
*/
|
||
const { execFileSync } = require('child_process');
|
||
|
||
const BASE = process.env.API_BASE || 'https://yjyf.gzyakj.com.cn/api';
|
||
let pass = 0, fail = 0;
|
||
const failures = [];
|
||
|
||
const ok = (m) => { pass++; console.log(' ✅ ' + m); };
|
||
const bad = (m, why) => { fail++; failures.push(m + ' :: ' + why); console.log(' ❌ ' + m + ' → ' + why); };
|
||
const chk = (c, m, why) => (c ? ok(m) : bad(m, why || '断言失败'));
|
||
const page = (t) => console.log('\n▌' + t);
|
||
|
||
async function api(path, opt) {
|
||
const o = opt || {};
|
||
const headers = {};
|
||
if (o.token) headers.token = o.token;
|
||
let body;
|
||
if (o.data) {
|
||
headers['content-type'] = 'application/x-www-form-urlencoded';
|
||
body = new URLSearchParams(o.data).toString();
|
||
}
|
||
const res = await fetch(BASE + path, { method: o.method || 'GET', headers, body });
|
||
const text = await res.text();
|
||
try { return JSON.parse(text); } catch (e) { throw new Error('非 JSON: ' + text.slice(0, 120)); }
|
||
}
|
||
const sql = (s) => execFileSync('ssh', ['-o', 'BatchMode=yes', 'root@8.137.181.63', `mysql -uyjyf -pyjyf yjyf -e "${s}"`], { encoding: 'utf8' }).trim();
|
||
|
||
(async () => {
|
||
const stamp = String(Date.now()).slice(-8);
|
||
const uM = '139' + stamp, mM = '137' + stamp;
|
||
let uToken = '', mToken = '', orderId = 0, masterId = 0;
|
||
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
console.log(' 优匠优服 · 从登录开始的逐页面全链路验证');
|
||
console.log(' 后端: ' + BASE);
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
|
||
// ── 页面 1:pages/login/login ────────────────────────────────
|
||
page('页面1 登录页 pages/login/login');
|
||
let r = await api('/auth/login', { method: 'POST', data: { mobile: uM, code: '123456' } });
|
||
chk(r.code === 1 && r.data.token, '手机号+验证码登录 → 拿到 token', r.msg);
|
||
uToken = r.data.token;
|
||
chk(r.data.userinfo.id > 0 && r.data.userinfo.is_master === false, '登录页读取 userinfo 正常(is_master=false)');
|
||
r = await api('/auth/login', { method: 'POST', data: { mobile: uM, code: '999999' } });
|
||
chk(r.code !== 1, '错误验证码被拒 → toast 文案可展示', r.msg);
|
||
|
||
// ── 页面 2:pages/index/index 首页 ─────────────────────────────────
|
||
page('页面2 首页 pages/index/index');
|
||
r = await api('/home/index', { token: uToken });
|
||
chk(r.code === 1 && r.data.cats.length === 15, '首页 15 个类目渲染数据就绪', '实际 ' + (r.data.cats || []).length);
|
||
chk(r.data.cats.every(c => c.name && c.icon), '每个类目都有 name + icon(图标库可匹配)');
|
||
chk(typeof r.data.badge.coupon === 'number' && typeof r.data.badge.unread === 'number', '首页角标 badge 字段就绪');
|
||
const cat0 = r.data.cats[0];
|
||
|
||
// ── 页面 3:pages/service/list ─────────────────────────────
|
||
page('页面3 服务列表 pages/service/list');
|
||
r = await api('/home/services?cat_id=' + cat0.id);
|
||
chk(r.code === 1 && r.data.list.length > 0, '按类目取服务列表(cat_id=' + cat0.id + ')', '共 ' + (r.data.list || []).length + ' 项');
|
||
const svc = r.data.list.find(s => s.price_type === 'fixed') || r.data.list[0];
|
||
chk(svc.price_text && svc.name, '列表卡片字段完整(name / price_text / sales)');
|
||
r = await api('/home/services?keyword=' + encodeURIComponent('安装'));
|
||
chk(r.code === 1 && r.data.list.length > 0, '首页搜索框 → keyword 检索可用', '命中 ' + (r.data.list || []).length + ' 项');
|
||
|
||
// ── 页面 4:pages/service/detail ───────────────────────────
|
||
page('页面4 服务详情 pages/service/detail');
|
||
r = await api('/home/detail?id=' + svc.id);
|
||
const detail = r.data.detail;
|
||
chk(r.code === 1 && detail.name === svc.name, '详情页服务信息加载', detail.name);
|
||
chk(detail.floor_fee >= 0 && detail.free_floor >= 0, '费用说明区块字段就绪(floor_fee / free_floor)');
|
||
|
||
// ── 页面 5:pages/order/create ─────────────────────────────
|
||
page('页面5 下单页 pages/order/create');
|
||
r = await api('/address/index', { token: uToken });
|
||
chk(r.code === 1, '进入下单页先拉地址列表');
|
||
r = await api('/home/calc?service_id=' + svc.id + '&floor=5&has_elevator=0&urgent=1', { token: uToken });
|
||
const c = r.data;
|
||
chk(c.floor_amount === 20 && c.urgent_amount === 16 && c.pay_amount === 116,
|
||
'费用试算实时联动(楼层20 + 加急16 = 116)', JSON.stringify({ f: c.floor_amount, u: c.urgent_amount, p: c.pay_amount }));
|
||
|
||
r = await api('/address/save', {
|
||
method: 'POST', token: uToken,
|
||
data: { contact: '页面验证用户', mobile: uM, province: '贵州省', city: '遵义市', district: '汇川区', address: '大连路 88 号 2 栋 501', floor: 5, has_elevator: 0, is_default: 1, lng: '106.92', lat: '27.72' }
|
||
});
|
||
chk(r.code === 1 && r.data.id > 0, '下单页内联新增地址 → 保存成功', 'addr_id=' + r.data.id);
|
||
const addrId = r.data.id;
|
||
|
||
r = await api('/order/create', {
|
||
method: 'POST', token: uToken,
|
||
data: { service_id: svc.id, address_id: addrId, appoint_time: Math.floor(Date.now() / 1000) + 7200, urgent: 1, goods_model: '三人位沙发', goods_num: 1, remark: '带工具' }
|
||
});
|
||
chk(r.code === 1 && r.data.order_id > 0, '提交订单 → 返回 order_id', r.data && r.data.order_no);
|
||
orderId = r.data.order_id;
|
||
|
||
r = await api('/order/pay', { method: 'POST', token: uToken, data: { order_id: orderId, pay_type: 'mock' } });
|
||
chk(r.code === 1, '自动支付 → 订单进入待接单', r.msg);
|
||
|
||
// ── 页面 6:pages/order/detail(用户侧首次查看) ──────────
|
||
page('页面6 订单详情 pages/order/detail(用户侧)');
|
||
r = await api('/order/detail?id=' + orderId, { token: uToken });
|
||
chk(r.data.order.status_text === '待接单', '详情页状态头显示「待接单」', r.data.order.status_text);
|
||
chk(r.data.logs.length >= 2, '轨迹时间线有数据', r.data.logs.length + ' 条');
|
||
chk(r.data.order.can_pay === false, '已支付 → 「去支付」按钮隐藏');
|
||
|
||
// ── 页面 7:pages/master/apply(师傅入驻) ─────────────────
|
||
page('页面7 师傅入驻 pages/master/apply');
|
||
r = await api('/auth/login', { method: 'POST', data: { mobile: mM, code: '123456' } });
|
||
mToken = r.data.token;
|
||
r = await api('/master/info', { token: mToken });
|
||
chk(r.code === 1 && r.data.applied === false, '未入驻 → 入驻页展示表单(applied=false)');
|
||
r = await api('/master/apply', {
|
||
method: 'POST', token: mToken,
|
||
data: { realname: '页面验证师傅', mobile: mM, idcard_no: '520101199203031234', city: '遵义市', 'skills[0]': cat0.id }
|
||
});
|
||
chk(r.code === 1 && r.data.master_id > 0, '提交实名+技能 → 进入待审核', 'master_id=' + r.data.master_id);
|
||
masterId = r.data.master_id;
|
||
r = await api('/master/info', { token: mToken });
|
||
chk(r.data.master.verify_status === 'pending', '入驻页显示「待审核」状态');
|
||
r = await api('/master/hall', { token: mToken });
|
||
chk(r.code !== 1, '审核中进入大厅被拦截 → 页面提示正确', r.msg);
|
||
|
||
sql(`UPDATE fa_master SET verify_status='pass' WHERE id=${masterId}`);
|
||
ok('(模拟平台后台审核通过)');
|
||
|
||
// ── 页面 8:pages/master/hall(订单大厅) ──────────────────
|
||
page('页面8 订单大厅 pages/master/hall');
|
||
r = await api('/master/info', { token: mToken });
|
||
chk(r.data.master.work_status === 'on', '大厅顶部展示「接单中」开关');
|
||
r = await api('/master/hall', { token: mToken });
|
||
const hallOrder = (r.data.list || []).find(o => Number(o.id) === orderId);
|
||
chk(!!hallOrder, '大厅列表能刷出该订单', '共 ' + r.data.total + ' 单');
|
||
chk(hallOrder.mobile.includes('****'), '大厅里客户手机号脱敏展示(抢单前)', hallOrder.mobile);
|
||
chk(Number(hallOrder.urgent) === 1, '急单标签展示');
|
||
|
||
r = await api('/master/grab', { method: 'POST', token: mToken, data: { order_id: orderId } });
|
||
chk(r.code === 1, '点击「立即抢单」成功', r.msg);
|
||
chk(r.data.user_mobile === uM, '抢单成功后才下发完整客户手机号');
|
||
|
||
// ── 页面 9:pages/master/orders(接单管理 / 履约) ──────────
|
||
page('页面9 接单管理 pages/master/orders');
|
||
r = await api('/master/orders?status=active', { token: mToken });
|
||
let mo = (r.data.list || []).find(o => Number(o.id) === orderId);
|
||
chk(!!mo && Number(mo.status) === 3, '「待处理」列表显示已接单', mo && mo.status_text);
|
||
|
||
r = await api('/master/contact', { method: 'POST', token: mToken, data: { order_id: orderId } });
|
||
chk(r.code === 1, '按钮「标记已联系」');
|
||
r = await api('/master/schedule', { method: 'POST', token: mToken, data: { order_id: orderId, appoint_time: Math.floor(Date.now() / 1000) + 1800 } });
|
||
chk(r.code === 1, '按钮「约定上门」→ 待上门');
|
||
r = await api('/master/arrive', { method: 'POST', token: mToken, data: { order_id: orderId, lng: '106.92', lat: '27.72' } });
|
||
chk(r.code === 1, '按钮「到达打卡」→ 已到达');
|
||
r = await api('/master/start', { method: 'POST', token: mToken, data: { order_id: orderId } }).catch(() => ({}));
|
||
if (!r.code) { r = await api('/master/startJob', { method: 'POST', token: mToken, data: { order_id: orderId } }); }
|
||
chk(r.code === 1, '按钮「开始服务」→ 服务中', r.msg);
|
||
r = await api('/master/finish', { method: 'POST', token: mToken, data: { order_id: orderId, 'images[0]': 'https://yjyf.gzyakj.com.cn/uploads/s.jpg' } });
|
||
chk(r.code === 1 && /^\d{4}$/.test(r.data.verify_code), '按钮「完工提交」→ 返回 4 位验收码', r.data && r.data.verify_code);
|
||
|
||
// ── 页面 6b:订单详情(用户侧验收) ────────────────────────
|
||
page('页面6b 订单详情 pages/order/detail(待验收态)');
|
||
r = await api('/order/detail?id=' + orderId, { token: uToken });
|
||
chk(Number(r.data.order.status) === 7, '状态头显示「待验收」', r.data.order.status_text);
|
||
chk(r.data.order.can_verify === true, '「确认验收」按钮出现');
|
||
chk(r.data.media.length === 1, '现场取证图片区块有数据');
|
||
|
||
r = await api('/order/verify', { method: 'POST', token: uToken, data: { order_id: orderId } });
|
||
chk(r.code === 1, '点击「确认验收」成功', r.msg);
|
||
r = await api('/order/detail?id=' + orderId, { token: uToken });
|
||
chk(Number(r.data.order.status) === 8, '状态头变为「已完成」', r.data.order.status_text);
|
||
chk(r.data.order.can_review === true, '「去评价」按钮出现');
|
||
|
||
// ── 页面 10:pages/order/review ────────────────────────────
|
||
page('页面10 评价页 pages/order/review');
|
||
r = await api('/review/create', {
|
||
method: 'POST', token: uToken,
|
||
data: { order_id: orderId, score: 5, content: '师傅专业,安装到位', tags: '准时,专业', is_anonymous: 0 }
|
||
});
|
||
chk(r.code === 1, '提交 5 星评价成功', r.msg);
|
||
r = await api('/review/masterList?master_id=' + masterId);
|
||
chk(r.code === 1 && r.data.total === 1 && Number(r.data.avg_score) === 5, '评价列表与师傅评分已刷新', 'avg=' + r.data.avg_score);
|
||
|
||
// ── 页面 11:pages/order/list(列表态回看) ─────────────────
|
||
page('页面11 我的订单 pages/order/list');
|
||
r = await api('/order/index?status=all', { token: uToken });
|
||
chk(r.code === 1 && r.data.total >= 1, '「全部」Tab 有数据', r.data.total + ' 单');
|
||
r = await api('/order/index?status=completed', { token: uToken });
|
||
const doneStatuses = (r.data.list || []).map(o => o.status + '/' + o.status_text);
|
||
chk((r.data.list || []).length > 0 && r.data.list.every(o => Number(o.status) === 8),
|
||
'「已完成」Tab 筛选正确', '返回 ' + doneStatuses.join(', '));
|
||
const item = r.data.list.find(o => Number(o.id) === orderId);
|
||
chk(item.can_review === false, '已评价 → 列表「去评价」按钮消失');
|
||
|
||
// ── 汇总 ───────────────────────────────────────────────────
|
||
console.log('\n═══════════════════════════════════════════════════════');
|
||
console.log(` 结果:通过 ${pass} 项 / 失败 ${fail} 项`);
|
||
if (fail) { console.log(' 失败明细:'); failures.forEach(f => console.log(' - ' + f)); process.exitCode = 1; }
|
||
else console.log(' 🎉 从登录到评价的全链路逐页面验证通过');
|
||
console.log(` 订单 orderId=${orderId} 师傅 masterId=${masterId}`);
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
})().catch(e => { console.error('\n💥 中断: ' + e.message); process.exitCode = 1; });
|