fastbee/yack-mini/tests/detail-console-toggles.test.js
2026-09-27 14:46:05 +08:00

206 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 回归:设备详情页底部收发控制台的三个开关(滚动 / 接收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();