feat(miniprogram): BLE 增强层 — DataBuffer + GenericBleAdapter + DataSyncScheduler
- DataBuffer: 离线持久化缓冲(分桶存储 + 去重 + 容量管理) - GenericBleAdapter: 基于 Bluetooth SIG 标准 Health Profile 的通用适配器 (Heart Rate 0x180D / Health Thermometer 0x1809 / Blood Pressure 0x1810) - DataSyncScheduler: 定时自动同步调度(基于时间间隔判断是否需要同步) - BLEManager: 集成 DataBuffer 替换简单 Storage 缓存 - device-sync 页面: 注册 CustomBandAdapter + 自动同步 + 状态显示 - 新增 vitest 单元测试配置,30 个测试全部通过
This commit is contained in:
89
apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts
Normal file
89
apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { DataBuffer } from '@/services/ble/DataBuffer';
|
||||
import type { NormalizedReading } from '@/services/ble/types';
|
||||
|
||||
// Mock Taro Storage
|
||||
const storage = new Map<string, string>();
|
||||
vi.mock('@tarojs/taro', () => ({
|
||||
default: {
|
||||
getStorageSync: vi.fn((key: string) => storage.get(key) || ''),
|
||||
setStorageSync: vi.fn((key: string, value: string) => { storage.set(key, value); }),
|
||||
removeStorageSync: vi.fn((key: string) => { storage.delete(key); }),
|
||||
getStorageInfoSync: vi.fn(() => ({ keys: Array.from(storage.keys()), limitSize: 10240, currentSize: storage.size })),
|
||||
},
|
||||
}));
|
||||
|
||||
function makeReading(overrides: Partial<NormalizedReading> = {}): NormalizedReading {
|
||||
return {
|
||||
device_type: 'heart_rate',
|
||||
values: { heart_rate: 72 },
|
||||
measured_at: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DataBuffer', () => {
|
||||
let buffer: DataBuffer;
|
||||
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
buffer = new DataBuffer({ bucketSize: 100 });
|
||||
});
|
||||
|
||||
it('push 添加读数并持久化', () => {
|
||||
const reading = makeReading();
|
||||
buffer.push(reading);
|
||||
expect(buffer.size()).toBe(1);
|
||||
});
|
||||
|
||||
it('push 批量添加读数', () => {
|
||||
const readings = Array.from({ length: 10 }, (_, i) =>
|
||||
makeReading({ measured_at: new Date(Date.now() + i * 1000).toISOString() }),
|
||||
);
|
||||
buffer.push(readings);
|
||||
expect(buffer.size()).toBe(10);
|
||||
});
|
||||
|
||||
it('flush 返回并清空缓冲区', () => {
|
||||
buffer.push([
|
||||
makeReading({ measured_at: '2026-05-04T10:00:00.000Z' }),
|
||||
makeReading({ measured_at: '2026-05-04T10:00:01.000Z' }),
|
||||
]);
|
||||
const flushed = buffer.flush();
|
||||
expect(flushed.length).toBe(2);
|
||||
expect(buffer.size()).toBe(0);
|
||||
});
|
||||
|
||||
it('超过 maxTotal 时丢弃最旧数据', () => {
|
||||
const smallBuffer = new DataBuffer({ bucketSize: 5, maxTotal: 10 });
|
||||
for (let i = 0; i < 15; i++) {
|
||||
smallBuffer.push(makeReading({ measured_at: new Date(i * 1000).toISOString() }));
|
||||
}
|
||||
expect(smallBuffer.size()).toBe(10);
|
||||
});
|
||||
|
||||
it('去重:相同 measured_at + device_type 不重复存储', () => {
|
||||
const ts = '2026-05-04T10:00:00.000Z';
|
||||
buffer.push(makeReading({ measured_at: ts }));
|
||||
buffer.push(makeReading({ measured_at: ts }));
|
||||
expect(buffer.size()).toBe(1);
|
||||
});
|
||||
|
||||
it('restore 从 Storage 恢复未上传数据', () => {
|
||||
buffer.push([
|
||||
makeReading({ measured_at: '2026-05-04T10:00:00.000Z' }),
|
||||
makeReading({ measured_at: '2026-05-04T10:00:01.000Z' }),
|
||||
]);
|
||||
// 模拟重启:新建 DataBuffer 并 restore
|
||||
const restored = new DataBuffer({ bucketSize: 100 });
|
||||
const count = restored.restore();
|
||||
expect(count).toBe(2);
|
||||
expect(restored.size()).toBe(2);
|
||||
});
|
||||
|
||||
it('clear 清空缓冲区和 Storage', () => {
|
||||
buffer.push(makeReading());
|
||||
buffer.clear();
|
||||
expect(buffer.size()).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user