308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
/**
|
||
* 假 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,
|
||
};
|