/** * 假 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, };