fastbee/yack-mini/tests/helpers/fake-wx.js
2026-09-27 14:46:05 +08:00

308 lines
10 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.

/**
* 假 wx 运行时(供 yack-mini 纯 JS 模块的 Node 回归测试使用)
*
* 设计要点(踩过的坑,勿改回去):
* - **storage 必须深拷贝**:真机 wx.setStorageSync 是序列化存储,直接存对象引用会让
* "读列表 → 改字段 → 存回"与内存里的对象互为别名,测出假的通过。
* - **native 桩必须能"失败"与"永不回调"**:被测状态机的分支几乎都由失败码驱动,
* 只回 success 会让一半用例假绿(链接不存在时要回 10002、适配器未开时要回 10001)。
* - **异步回调**:wx 的回调都是异步的,同步触发会打乱"先注册监听再连接"的顺序。
* - 只暴露被测代码真正会用到的 API,**新增 wx.onXxx 用法时只改这一个文件**。
*/
const path = require('path');
const MINI_ROOT = path.resolve(__dirname, '../../miniprogram');
/* ============================ 存储 ============================ */
function createStorageArea() {
const map = new Map();
const clone = (v) => (v === undefined ? v : JSON.parse(JSON.stringify(v)));
return {
_map: map,
getStorageSync: (k) => (map.has(k) ? clone(map.get(k)) : ''),
setStorageSync: (k, v) => { map.set(k, clone(v)); },
removeStorageSync: (k) => { map.delete(k); },
clearStorageSync: () => map.clear(),
};
}
/* ============================ BLE 桩 ============================ */
// 一个非通用(非 1800/1801/180a/180f)服务,带 1 个 notify + 1 个 write 特征,
// 让 _discoverServices/_discoverCharacteristics 能走到 READY
const SVC = '0000FFF0-0000-1000-8000-00805F9B34FB';
const CHAR_NOTIFY = '0000FFF1-0000-1000-8000-00805F9B34FB';
const CHAR_WRITE = '0000FFF2-0000-1000-8000-00805F9B34FB';
function createBleState() {
return {
adapterOpen: false,
links: new Set(), // 已建立的链路 deviceId(createBLEConnection 成功才加入)
createMode: 'ok', // 'ok' | 'hang'(native 永不回调)| 'fail'
createErrMsg: '连接超时',
listeners: { conn: [], char: [], adapter: [] },
calls: {
openAdapter: 0, closeAdapter: 0,
create: [], closeConn: [], mtu: [], services: [], chars: [], notify: [],
},
};
}
function createBleApi(st) {
const emitConn = (deviceId, connected) => {
st.listeners.conn.forEach((cb) => cb({ deviceId, connected }));
};
return {
onBLEConnectionStateChange: (cb) => st.listeners.conn.push(cb),
onBLECharacteristicValueChange: (cb) => st.listeners.char.push(cb),
onBluetoothAdapterStateChange: (cb) => st.listeners.adapter.push(cb),
openBluetoothAdapter: (o = {}) => {
st.calls.openAdapter++;
st.adapterOpen = true;
setTimeout(() => o.success && o.success({}), 0);
},
closeBluetoothAdapter: (o = {}) => {
st.calls.closeAdapter++;
st.adapterOpen = false;
// native 关适配器会同时拆掉全部链路并派发断链事件(实测如此)
const ids = Array.from(st.links);
st.links.clear();
setTimeout(() => {
ids.forEach((id) => emitConn(id, false));
o.success && o.success({});
o.complete && o.complete({});
}, 0);
},
createBLEConnection: (o = {}) => {
st.calls.create.push(o.deviceId);
if (st.createMode === 'hang') return; // 坏桩:永不回调(测超时兜底)
setTimeout(() => {
if (st.createMode === 'fail') {
o.fail && o.fail({ errCode: 10003, errMsg: st.createErrMsg });
return;
}
st.links.add(o.deviceId);
o.success && o.success({});
}, 0);
},
closeBLEConnection: (o = {}) => {
st.calls.closeConn.push(o.deviceId);
const had = st.links.has(o.deviceId);
setTimeout(() => {
if (!had) {
// 链路本就不存在:真机回 fail 且**不派发** onBLEConnectionStateChange
o.fail && o.fail({ errCode: 10002, errMsg: 'no connection' });
o.complete && o.complete({});
return;
}
st.links.delete(o.deviceId);
emitConn(o.deviceId, false);
o.success && o.success({});
o.complete && o.complete({});
}, 0);
},
setBLEMTU: (o = {}) => {
st.calls.mtu.push(o.deviceId);
setTimeout(() => { o.success && o.success({}); o.complete && o.complete({}); }, 0);
},
getBLEDeviceServices: (o = {}) => {
st.calls.services.push(o.deviceId);
setTimeout(() => {
if (!st.links.has(o.deviceId)) {
// 链路不存在时必须失败,否则"GATT 探活"永远返回 alive(死链发现不了)
o.fail && o.fail({ errCode: 10002, errMsg: 'no connection' });
return;
}
o.success && o.success({
services: [
{ uuid: '00001800-0000-1000-8000-00805F9B34FB', isPrimary: true },
{ uuid: SVC, isPrimary: true },
],
});
}, 0);
},
getBLEDeviceCharacteristics: (o = {}) => {
st.calls.chars.push(o.characteristicId || o.serviceId);
setTimeout(() => {
if (!st.links.has(o.deviceId)) {
o.fail && o.fail({ errCode: 10002, errMsg: 'no connection' });
return;
}
o.success && o.success({
characteristics: [
{ uuid: CHAR_NOTIFY, properties: { notify: true, read: true } },
{ uuid: CHAR_WRITE, properties: { write: true, writeNoResponse: true } },
],
});
}, 0);
},
notifyBLECharacteristicValueChange: (o = {}) => {
st.calls.notify.push(o.characteristicId);
setTimeout(() => o.success && o.success({}), 0);
},
getSystemInfoSync: () => ({ platform: 'android' }),
};
}
/* ============================ wx 组装 ============================ */
function createWx(storageArea, bleState) {
return {
...createBleApi(bleState),
...storageArea,
arrayBufferToBase64: (buf) => Buffer.from(new Uint8Array(buf)).toString('base64'),
base64ToArrayBuffer: (b64) => {
const buf = Buffer.from(b64, 'base64');
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
},
getNetworkType: (o = {}) => setTimeout(() => o.success && o.success({ networkType: 'wifi' }), 0),
onNetworkStatusChange: () => {},
showToast: () => {},
showModal: () => {},
showLoading: () => {},
hideLoading: () => {},
};
}
/* ============================ 模块加载 ============================ */
function clearMiniCache() {
Object.keys(require.cache).forEach((k) => {
if (k.startsWith(MINI_ROOT + path.sep) || k.startsWith(MINI_ROOT + '/')) delete require.cache[k];
});
}
function stubModule(id, exports) {
return { id, filename: id, loaded: true, exports, children: [], paths: [] };
}
/**
* 假 MQTT 客户端(隔离无关依赖:只记录"被要求建连"的次数)
*/
function createFakeMqtt() {
const rec = { connectCalls: [], publishCalls: [], disconnectCalls: 0, reconnectCalls: 0 };
let held = false; // 是否已持有客户端(canAutoConnect 语义:持有即视为已连/自愈中)
const client = {
userDisconnected: false,
isConnected: () => false,
getState: () => 'disconnected',
canAutoConnect: () => !client.userDisconnected && !held,
onStateChange: () => {},
connect: (opts) => {
rec.connectCalls.push((opts && opts.clientId) || '');
held = true;
return Promise.resolve();
},
reconnect: (opts) => {
rec.reconnectCalls++;
rec.connectCalls.push((opts && opts.clientId) || '');
held = true;
return Promise.resolve();
},
disconnect: () => {
rec.disconnectCalls++;
client.userDisconnected = true;
held = false;
return Promise.resolve();
},
publish: (topic, message) => {
rec.publishCalls.push({ topic, message });
return Promise.resolve();
},
};
return {
MQTT_STATE: { DISCONNECTED: 'disconnected', CONNECTING: 'connecting', CONNECTED: 'connected' },
mqttClient: client,
_rec: rec,
};
}
/**
* 准备一套干净的运行时并加载被测模块
* @param {Object} [opts]
* @param {'ok'|'hang'|'fail'} [opts.createMode] - 建链桩行为
* @returns {Object} { wx, state, storageArea, gateway, bleService, storage, measurePoint, mqtt, GATEWAY_PHASE, BLE_STATE }
*/
function loadRuntime(opts = {}) {
clearMiniCache();
const storageArea = createStorageArea();
const bleState = createBleState();
if (opts.createMode) bleState.createMode = opts.createMode;
global.wx = createWx(storageArea, bleState);
const mqtt = createFakeMqtt();
const mqttPath = require.resolve(path.join(MINI_ROOT, 'services/mqtt-client.js'));
require.cache[mqttPath] = stubModule(mqttPath, {
MQTT_STATE: mqtt.MQTT_STATE,
mqttClient: mqtt.mqttClient,
});
const { gateway, GATEWAY_PHASE } = require(path.join(MINI_ROOT, 'services/gateway.js'));
const { bleService, BLE_STATE } = require(path.join(MINI_ROOT, 'services/ble.js'));
const storage = require(path.join(MINI_ROOT, 'utils/storage.js'));
const measurePoint = require(path.join(MINI_ROOT, 'utils/measure-point.js'));
return {
wx: global.wx,
state: bleState,
storageArea,
mqtt,
gateway,
bleService,
storage,
measurePoint,
GATEWAY_PHASE,
BLE_STATE,
};
}
/**
* 造一个已登录 + 已配 MQTT 凭证 + 一台启用设备的常规现场
*/
function seedScene(rt, { serialNumber = 'BT_ECB0D1002F84', deviceId = 'EC:B0:D1:00:2F:84', enabled = true } = {}) {
rt.storage.setUserInfo({ userId: 1, userName: 'tester' });
rt.storage.setMqttConfig({ serialNumber, productId: 12, productSecret: 'secret' });
rt.measurePoint.setMeasurePointId('371883');
rt.storage.addDevice({
deviceId,
serialNumber,
name: serialNumber,
bleName: serialNumber,
enabled,
connected: false,
createTime: Date.now(),
});
if (enabled) rt.storage.activateDevice(serialNumber);
return rt.storage.getDevice(serialNumber);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
module.exports = {
MINI_ROOT,
SVC,
CHAR_NOTIFY,
CHAR_WRITE,
createStorageArea,
createBleState,
createWx,
createFakeMqtt,
clearMiniCache,
stubModule,
loadRuntime,
seedScene,
sleep,
};