206 lines
8.8 KiB
JavaScript
206 lines
8.8 KiB
JavaScript
/**
|
||
* 回归:设备详情页底部收发控制台的三个开关(滚动 / 接收Hex / 发送Hex)
|
||
*
|
||
* 缺陷(修复前):
|
||
* 三处都是「裸 checkbox + bindchange」——而 bindchange 是 checkbox-group 的事件,
|
||
* checkbox 自身不提供(官方属性表只有 value/disabled/checked/color)。
|
||
* handler 又按 group 的数组语义写成 `e.detail.value.length > 0`:
|
||
* 布尔值下 `true.length === undefined` → `undefined > 0 === false`,三个开关恒定失效。
|
||
* 现场表现:取消「接收Hex」切不到 ASCII(曾被误判成 asciiOfHex 显示「·」的设计行为)、
|
||
* 「滚动」关不掉也开不了、发送区一直按 hex 语义走。
|
||
*
|
||
* 红线(本套用例锁死):
|
||
* 1. wxml 结构:每个 checkbox 必须在 checkbox-group 内,group 各绑一个 change
|
||
* 2. detail.js 不得回退成 `e.detail.value.length > 0` 这种数组语义误用
|
||
* 3. group 派发 value 数组时,三个开关能正确切换 data
|
||
* 4. 取消「接收Hex」→ 日志切成 ASCII 视图;勾上 → 回到 hex 视图(用最近快照重渲染)
|
||
* 5. 取消「滚动」→ autoScroll 与 logFollowing 同时关闭;勾上 → 同时恢复
|
||
*
|
||
* 跑法:node tests/detail-console-toggles.test.js
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { MINI_ROOT, loadRuntime } = require('./helpers/fake-wx.js');
|
||
|
||
/* ============================ 迷你断言器 ============================ */
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function ok(cond, label, extra) {
|
||
if (cond) {
|
||
passed++;
|
||
process.stdout.write(` ✓ ${label}\n`);
|
||
} else {
|
||
failed++;
|
||
process.stdout.write(` ✗ ${label}${extra !== undefined ? ` ← ${JSON.stringify(extra)}` : ''}\n`);
|
||
}
|
||
}
|
||
|
||
function eq(actual, expected, label) {
|
||
ok(actual === expected, `${label} (期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)})`);
|
||
}
|
||
|
||
/* ============================ 被测文件与页面实例 ============================ */
|
||
|
||
const DETAIL_DIR = path.join(MINI_ROOT, 'pages/device/detail');
|
||
const WXML = fs.readFileSync(path.join(DETAIL_DIR, 'detail.wxml'), 'utf8');
|
||
const DETAIL_JS = fs.readFileSync(path.join(DETAIL_DIR, 'detail.js'), 'utf8');
|
||
const DETAIL_SRC = path.join(DETAIL_DIR, 'detail.js');
|
||
|
||
/**
|
||
* 每次调用都重建一套干净的运行时 + 全新的页面实例,
|
||
* 用例之间不共享 data,避免"上一条用例把开关改过"造成的假绿/假红
|
||
*/
|
||
function loadPage() {
|
||
const rt = loadRuntime(); // 内部 clearMiniCache + 重建 global.wx
|
||
global.getApp = () => ({ globalData: {} });
|
||
let cfg = null;
|
||
global.Page = (c) => { cfg = c; };
|
||
require(DETAIL_SRC);
|
||
if (!cfg) throw new Error('detail.js 未调用 Page(),加载失败');
|
||
|
||
const page = Object.assign({}, cfg);
|
||
page.data = JSON.parse(JSON.stringify(cfg.data));
|
||
page.setData = function (patch, cb) {
|
||
Object.assign(this.data, patch);
|
||
if (cb) cb();
|
||
};
|
||
return { rt, page };
|
||
}
|
||
|
||
/* ============================ 用例 ============================ */
|
||
|
||
/** 结构红线:勾选框必须被 checkbox-group 包着 */
|
||
function case1_wxmlStructure() {
|
||
process.stdout.write('\n[1] wxml 结构:checkbox 必须在 checkbox-group 内\n');
|
||
|
||
const groups = WXML.match(/<checkbox-group[^>]*>[\s\S]*?<\/checkbox-group>/g) || [];
|
||
eq(groups.length, 3, '三个开关各有一个 checkbox-group');
|
||
eq((WXML.match(/<checkbox[\s>]/g) || []).length, 3, '页面共 3 个 checkbox');
|
||
|
||
ok(!/<checkbox\s[^>]*bindchange/.test(WXML),
|
||
'没有任何「裸 checkbox 直接挂 bindchange」的写法(这是本次缺陷的形态)');
|
||
|
||
ok(groups.some((g) => g.includes('bindchange="onAutoScrollToggle"') && g.includes('value="scroll"')),
|
||
'「滚动」group 绑 onAutoScrollToggle,子项 value=scroll');
|
||
ok(groups.some((g) => g.includes('bindchange="onRecvHexToggle"') && g.includes('value="hex"')),
|
||
'接收「Hex」group 绑 onRecvHexToggle,子项 value=hex');
|
||
ok(groups.some((g) => g.includes('bindchange="onSendHexToggle"') && g.includes('value="hex"')),
|
||
'发送「Hex」group 绑 onSendHexToggle,子项 value=hex');
|
||
}
|
||
|
||
/** 代码红线:不得回退成数组语义误用 */
|
||
function case2_codeGuards() {
|
||
process.stdout.write('\n[2] detail.js 取值方式:读 group 的 value 数组,而非误判为布尔\n');
|
||
|
||
ok(!/\.detail\.value\.length\s*>\s*0/.test(DETAIL_JS),
|
||
'没有 `e.detail.value.length > 0` 的残留(该写法在布尔值下恒 false)');
|
||
|
||
ok(/onAutoScrollToggle\(e\)\s*\{[^}]*includes\('scroll'\)/.test(DETAIL_JS),
|
||
'onAutoScrollToggle 用 includes(\'scroll\') 读值');
|
||
ok(/onRecvHexToggle\(e\)\s*\{[^}]*includes\('hex'\)/.test(DETAIL_JS),
|
||
'onRecvHexToggle 用 includes(\'hex\') 读值');
|
||
ok(/onSendHexToggle\(e\)\s*\{[^}]*includes\('hex'\)/.test(DETAIL_JS),
|
||
'onSendHexToggle 用 includes(\'hex\') 读值');
|
||
|
||
// 反例固化:裸 checkbox 若派发布尔值,旧写法必然恒 false —— 本次缺陷的数学根因
|
||
const legacy = (v) => v.length > 0;
|
||
eq(legacy(true), false, '反例:布尔 true 在旧写法下取到 false');
|
||
eq(legacy(false), false, '反例:布尔 false 在旧写法下取到 false');
|
||
}
|
||
|
||
/** 滚动开关:勾选/取消都要同时管住 autoScroll 与 logFollowing */
|
||
function case3_autoScrollToggle() {
|
||
process.stdout.write('\n[3] 「滚动」开关\n');
|
||
const { page } = loadPage();
|
||
|
||
eq(page.data.autoScroll, true, '初始勾选(默认自动跟随)');
|
||
eq(page.data.logFollowing, true, '初始处于跟随态');
|
||
|
||
page.onAutoScrollToggle({ detail: { value: [] } }); // 用户取消勾选
|
||
eq(page.data.autoScroll, false, '取消勾选 → 停止自动滚动');
|
||
eq(page.data.logFollowing, false, '取消勾选 → 同时退出跟随态');
|
||
|
||
page.onAutoScrollToggle({ detail: { value: ['scroll'] } }); // 用户重新勾选
|
||
eq(page.data.autoScroll, true, '重新勾选 → 恢复自动滚动');
|
||
eq(page.data.logFollowing, true, '重新勾选 → 恢复跟随态');
|
||
}
|
||
|
||
/** 接收 Hex 开关:切换 data 并用最近快照重渲染日志视图 */
|
||
function case4_recvHexToggle() {
|
||
process.stdout.write('\n[4] 接收「Hex」开关(含视图重渲染)\n');
|
||
const { page } = loadPage();
|
||
|
||
// 造一份最近快照:gateway 的 dataList 最新在前
|
||
page.data.device = { serialNumber: 'BT_1' };
|
||
page._lastSnap = {
|
||
device: { serialNumber: 'BT_1' },
|
||
dataList: [
|
||
{ id: '2', hex: '41424344' }, // 最新一帧
|
||
{ id: '1', hex: '31323334' },
|
||
],
|
||
};
|
||
|
||
eq(page.data.recvHex, true, '初始勾选(hex 视图)');
|
||
|
||
page.onRecvHexToggle({ detail: { value: [] } }); // 取消勾选 → ASCII 视图
|
||
eq(page.data.recvHex, false, '取消勾选 → 切到 ASCII 视图');
|
||
eq(page.data.displayList.length, 2, 'ASCII 视图:用最近快照重渲染出 2 行');
|
||
eq(page.data.displayList[1].text, 'ABCD', '最新在后:末行为最新帧的 ASCII 文本');
|
||
eq(page.data.displayList[0].text, '1234', 'ASCII 视图:可打印字节原样显示');
|
||
|
||
page.onRecvHexToggle({ detail: { value: ['hex'] } }); // 勾上 → 回 hex 视图
|
||
eq(page.data.recvHex, true, '勾选 → 回到 hex 视图');
|
||
eq(page.data.displayList[1].text, '41424344', 'hex 视图:末行为原始 hex 帧');
|
||
|
||
// 非本设备的快照不得上屏
|
||
page._lastSnap = { device: { serialNumber: 'BT_OTHER' }, dataList: [{ id: '9', hex: 'AA' }] };
|
||
page.onRecvHexToggle({ detail: { value: [] } });
|
||
eq(page.data.displayList.length, 0, '非本设备快照:不上屏(保持空列表)');
|
||
}
|
||
|
||
/** 发送 Hex 开关:切换语义 + 已有输入大小写归一 */
|
||
function case5_sendHexToggle() {
|
||
process.stdout.write('\n[5] 发送「Hex」开关\n');
|
||
const { page } = loadPage();
|
||
|
||
eq(page.data.sendHex, true, '初始勾选(按 hex 字节发送)');
|
||
|
||
page.data.sendInput = 'at+csq';
|
||
page.onSendHexToggle({ detail: { value: [] } }); // 取消 → ASCII 文本发送
|
||
eq(page.data.sendHex, false, '取消勾选 → 按 ASCII 文本发送');
|
||
eq(page.data.sendInput, 'at+csq', 'ASCII 模式保留原文大小写(大小写是内容)');
|
||
|
||
page.data.sendInput = 'at+csq';
|
||
page.onSendHexToggle({ detail: { value: ['hex'] } }); // 勾上 → hex 语义
|
||
eq(page.data.sendHex, true, '勾选 → 按 hex 字节发送');
|
||
eq(page.data.sendInput, 'AT+CSQ', 'hex 模式统一大写(便于阅读)');
|
||
}
|
||
|
||
/* ============================ 主流程 ============================ */
|
||
|
||
function main() {
|
||
process.stdout.write('===== 详情页收发控制台开关 回归 =====\n');
|
||
const cases = [
|
||
case1_wxmlStructure,
|
||
case2_codeGuards,
|
||
case3_autoScrollToggle,
|
||
case4_recvHexToggle,
|
||
case5_sendHexToggle,
|
||
];
|
||
for (const c of cases) {
|
||
try {
|
||
c();
|
||
} catch (e) {
|
||
failed++;
|
||
process.stdout.write(` ✗ 用例抛异常: ${e && e.stack}\n`);
|
||
}
|
||
}
|
||
|
||
process.stdout.write(`\n===== 详情页收发控制台开关:通过 ${passed} / 失败 ${failed} =====\n`);
|
||
process.exit(failed ? 1 : 0);
|
||
}
|
||
|
||
main();
|