/** * 回归:设备详情页底部收发控制台的三个开关(滚动 / 接收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(/]*>[\s\S]*?<\/checkbox-group>/g) || []; eq(groups.length, 3, '三个开关各有一个 checkbox-group'); eq((WXML.match(/]/g) || []).length, 3, '页面共 3 个 checkbox'); ok(!/]*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();