* fix: resolve lint warnings/errors and apply formatting Fix 10 oxlint issues (2 errors, 8 warnings): - Remove unused catch parameters in Security.tsx and ImportDialog.tsx - Remove stray expression in Generator.tsx - Add eslint-disable for debounced useCallback in FilterControl.tsx - Remove unnecessary deps (resolveDB, store) in bindStoreToDevice.ts - Prefix unused variant param in AppSidebar.tsx - Memoize tabs arrays in DeviceConfig, RadioConfig, ModuleConfig - Fix channels type in RadioConfig TabItem Also applies oxfmt formatting across all files. * updating lock file * update protobuf package * fix: regenerate pnpm-lock.yaml and exclude jsr protobufs from minimumReleaseAge * prevented http card from always being polled * updated pnpm config fiile * updated actions * removed biome config and lint system leftovers * updating protobuf package
176 lines
5.8 KiB
TypeScript
176 lines
5.8 KiB
TypeScript
import { describe, expect, vi } from "vitest";
|
|
import { runTransportContract } from "../../../tests/utils/transportContract.ts";
|
|
import { TransportWebBluetooth } from "./transport.ts";
|
|
|
|
class MiniEmitter {
|
|
private listeners = new Map<string, Set<(e: Event) => void>>();
|
|
addEventListener(type: string, listener: (e: Event) => void) {
|
|
if (!this.listeners.has(type)) {
|
|
this.listeners.set(type, new Set());
|
|
}
|
|
this.listeners.get(type)!.add(listener);
|
|
}
|
|
removeEventListener(type: string, listener: (e: Event) => void) {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
dispatchEvent(event: Event) {
|
|
this.listeners.get(event.type)?.forEach((l) => {
|
|
l(event);
|
|
});
|
|
}
|
|
}
|
|
|
|
function stubWebBluetooth() {
|
|
const incomingQueue: Uint8Array[] = [];
|
|
let lastWritten: Uint8Array | undefined;
|
|
|
|
// fromRadioCharacteristic: read bytes from queue, one buffer per read
|
|
const fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic = {
|
|
async readValue() {
|
|
const next = incomingQueue.shift() ?? new Uint8Array();
|
|
return new DataView(next.buffer, next.byteOffset, next.byteLength) as unknown as DataView;
|
|
},
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
} as unknown as BluetoothRemoteGATTCharacteristic;
|
|
|
|
// characteristicvaluechanged event plumbing (fromNumCharacteristic)
|
|
const charEmitter = new MiniEmitter();
|
|
|
|
const fromNumCharacteristic: BluetoothRemoteGATTCharacteristic = {
|
|
async startNotifications() {
|
|
return this;
|
|
},
|
|
addEventListener(type: string, listener: (e: Event) => void) {
|
|
charEmitter.addEventListener(type, listener);
|
|
},
|
|
removeEventListener(type: string, listener: (e: Event) => void) {
|
|
charEmitter.removeEventListener(type, listener);
|
|
},
|
|
} as unknown as BluetoothRemoteGATTCharacteristic;
|
|
|
|
const toRadioCharacteristic: BluetoothRemoteGATTCharacteristic = {
|
|
async writeValue(bufferSource: BufferSource) {
|
|
const u8 =
|
|
bufferSource instanceof ArrayBuffer
|
|
? new Uint8Array(bufferSource)
|
|
: new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, bufferSource.byteLength);
|
|
lastWritten = new Uint8Array(u8);
|
|
},
|
|
} as unknown as BluetoothRemoteGATTCharacteristic;
|
|
|
|
// Primary service returns our three characteristics by UUID
|
|
const primaryService: BluetoothRemoteGATTService = {
|
|
async getCharacteristic(uuid: string) {
|
|
if (uuid === TransportWebBluetooth.ToRadioUuid) {
|
|
return toRadioCharacteristic;
|
|
}
|
|
if (uuid === TransportWebBluetooth.FromRadioUuid) {
|
|
return fromRadioCharacteristic;
|
|
}
|
|
if (uuid === TransportWebBluetooth.FromNumUuid) {
|
|
return fromNumCharacteristic;
|
|
}
|
|
throw new Error(`Unknown characteristic: ${uuid}`);
|
|
},
|
|
} as unknown as BluetoothRemoteGATTService;
|
|
|
|
// Device-level emitter to deliver gattserverdisconnected
|
|
const deviceEmitter = new MiniEmitter();
|
|
|
|
// GATT server with readonly connected
|
|
let isConnected = true;
|
|
const gattServer: BluetoothRemoteGATTServer = {
|
|
get connected() {
|
|
return isConnected;
|
|
},
|
|
async connect() {
|
|
isConnected = true;
|
|
return gattServer;
|
|
},
|
|
disconnect() {
|
|
isConnected = false;
|
|
deviceEmitter.dispatchEvent(new Event("gattserverdisconnected"));
|
|
},
|
|
async getPrimaryService() {
|
|
return primaryService;
|
|
},
|
|
device: {
|
|
addEventListener: (...args: Parameters<EventTarget["addEventListener"]>) =>
|
|
deviceEmitter.addEventListener(args[0] as string, args[1] as (e: Event) => void),
|
|
removeEventListener: (...args: Parameters<EventTarget["removeEventListener"]>) =>
|
|
deviceEmitter.removeEventListener(args[0] as string, args[1] as (e: Event) => void),
|
|
} as unknown as BluetoothDevice,
|
|
} as unknown as BluetoothRemoteGATTServer;
|
|
|
|
const fakeDevice: BluetoothDevice = {
|
|
async watchAdvertisements() {},
|
|
gatt: gattServer,
|
|
} as unknown as BluetoothDevice;
|
|
|
|
const fakeNavigator = {
|
|
bluetooth: {
|
|
async requestDevice() {
|
|
return fakeDevice;
|
|
},
|
|
},
|
|
};
|
|
|
|
vi.stubGlobal("navigator", Object.assign({}, globalThis.navigator, fakeNavigator));
|
|
|
|
// helper actions for tests/contract
|
|
return {
|
|
pushIncoming: (u8: Uint8Array) => {
|
|
incomingQueue.push(u8);
|
|
charEmitter.dispatchEvent(new Event("characteristicvaluechanged"));
|
|
},
|
|
assertLastWritten: (u8: Uint8Array) => {
|
|
expect(lastWritten).toBeDefined();
|
|
expect(lastWritten).toEqual(u8);
|
|
},
|
|
// simulate underlying link drop (OS-level disconnect)
|
|
triggerGattDisconnect: () => {
|
|
isConnected = false;
|
|
deviceEmitter.dispatchEvent(new Event("gattserverdisconnected"));
|
|
},
|
|
cleanup: () => {
|
|
vi.unstubAllGlobals();
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("TransportWebBluetooth (contract)", () => {
|
|
runTransportContract({
|
|
name: "TransportWebBluetooth",
|
|
setup: () => {},
|
|
teardown: () => {
|
|
(globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }).__ble?.cleanup();
|
|
(globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }).__ble = undefined;
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
},
|
|
create: async () => {
|
|
(globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }).__ble =
|
|
stubWebBluetooth();
|
|
return await TransportWebBluetooth.create();
|
|
},
|
|
pushIncoming: async (bytes) => {
|
|
(globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }).__ble.pushIncoming(
|
|
bytes,
|
|
);
|
|
await Promise.resolve();
|
|
},
|
|
assertLastWritten: (bytes) => {
|
|
(
|
|
globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }
|
|
).__ble.assertLastWritten(bytes);
|
|
},
|
|
triggerDisconnect: async () => {
|
|
(
|
|
globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }
|
|
).__ble.triggerGattDisconnect();
|
|
await Promise.resolve();
|
|
},
|
|
});
|
|
});
|