Skip to content

Commit 55e26b8

Browse files
Send previous status in connection events (#74)
This is trivial for the connections and saves apps from additional state management as they often need to understand which transition happened.
1 parent 90a9e79 commit 55e26b8

File tree

5 files changed

+132
-12
lines changed

5 files changed

+132
-12
lines changed

lib/bluetooth.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* (c) 2021, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
import { ConnectionStatus, ConnectionStatusEvent } from "./device.js";
7+
import { createWebBluetoothConnection } from "./bluetooth.js";
8+
import { expect, vi, describe, it } from "vitest";
9+
10+
// Mock Capacitor
11+
vi.mock("@capacitor/core", () => ({
12+
Capacitor: {
13+
isNativePlatform: () => false,
14+
getPlatform: () => "web",
15+
},
16+
}));
17+
18+
// Mock BleClient
19+
vi.mock("@capacitor-community/bluetooth-le", () => ({
20+
BleClient: {
21+
initialize: vi.fn().mockResolvedValue(undefined),
22+
isEnabled: vi.fn().mockResolvedValue(true),
23+
requestDevice: vi.fn(),
24+
connect: vi.fn(),
25+
disconnect: vi.fn().mockResolvedValue(undefined),
26+
getServices: vi.fn().mockResolvedValue([]),
27+
},
28+
}));
29+
30+
// Mock flashing modules
31+
vi.mock("./flashing/flashing-partial.js", () => ({
32+
default: vi.fn(),
33+
PartialFlashResult: { AttemptFullFlash: "AttemptFullFlash" },
34+
}));
35+
36+
vi.mock("./flashing/flashing-full.js", () => ({
37+
fullFlash: vi.fn(),
38+
}));
39+
40+
const setupNavigatorMock = () => {
41+
Object.defineProperty(globalThis, "navigator", {
42+
value: {
43+
bluetooth: {
44+
getAvailability: vi.fn().mockResolvedValue(true),
45+
addEventListener: vi.fn(),
46+
removeEventListener: vi.fn(),
47+
requestDevice: vi.fn(),
48+
},
49+
},
50+
writable: true,
51+
configurable: true,
52+
});
53+
};
54+
55+
describe("Bluetooth connection status events", () => {
56+
it("emits status events with correct previousStatus", async () => {
57+
setupNavigatorMock();
58+
const connection = createWebBluetoothConnection();
59+
await connection.initialize();
60+
61+
expect(connection.status).toBe(ConnectionStatus.NO_AUTHORIZED_DEVICE);
62+
63+
const events: ConnectionStatusEvent[] = [];
64+
connection.addEventListener("status", (e) => {
65+
events.push(e);
66+
});
67+
68+
await connection.disconnect();
69+
70+
expect(events).toHaveLength(1);
71+
expect(events[0].status).toBe(ConnectionStatus.DISCONNECTED);
72+
expect(events[0].previousStatus).toBe(
73+
ConnectionStatus.NO_AUTHORIZED_DEVICE,
74+
);
75+
});
76+
77+
it("defers status updates during flash, emitting single catch-up event", async () => {
78+
setupNavigatorMock();
79+
const connection = createWebBluetoothConnection();
80+
await connection.initialize();
81+
82+
const statusBeforeFlash = connection.status;
83+
expect(statusBeforeFlash).toBe(ConnectionStatus.NO_AUTHORIZED_DEVICE);
84+
85+
const events: ConnectionStatusEvent[] = [];
86+
connection.addEventListener("status", (e) => {
87+
events.push(e);
88+
});
89+
90+
// flash() will fail but the finally block still emits a catch-up event
91+
try {
92+
await connection.flash(async () => "mock-hex-data", {});
93+
} catch {
94+
// Expected to fail
95+
}
96+
97+
// Should have exactly one catch-up event with correct previousStatus
98+
expect(events).toHaveLength(1);
99+
expect(events[0].previousStatus).toBe(statusBeforeFlash);
100+
});
101+
});

lib/bluetooth.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ class MicrobitWebBluetoothConnectionImpl
196196
private connection: BluetoothDeviceWrapper | undefined;
197197

198198
private nameFilter: string | undefined;
199-
private deferStatusUpdates: boolean = false;
199+
private deferredUpdatesPreviousStatus: ConnectionStatus | undefined;
200200

201201
constructor(options: MicrobitWebBluetoothConnectionOptions = {}) {
202202
super();
@@ -345,10 +345,14 @@ class MicrobitWebBluetoothConnectionImpl
345345
}
346346

347347
private setStatus(newStatus: ConnectionStatus) {
348+
const previousStatus = this.status;
348349
this.status = newStatus;
349350
this.log("Bluetooth connection status " + newStatus);
350-
if (!this.deferStatusUpdates) {
351-
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
351+
if (this.deferredUpdatesPreviousStatus === undefined) {
352+
this.dispatchTypedEvent(
353+
"status",
354+
new ConnectionStatusEvent(newStatus, previousStatus),
355+
);
352356
}
353357
}
354358

@@ -470,7 +474,7 @@ class MicrobitWebBluetoothConnectionImpl
470474
const progress: ProgressCallback = options.progress ?? (() => {});
471475
try {
472476
// We'll disconnect/reconnect multiple times due to device resets, but reporting this is unhelpful.
473-
this.deferStatusUpdates = true;
477+
this.deferredUpdatesPreviousStatus = this.status;
474478

475479
if (this.status !== ConnectionStatus.CONNECTED) {
476480
await this.connect({ progress });
@@ -516,8 +520,12 @@ class MicrobitWebBluetoothConnectionImpl
516520
await this.disconnect();
517521
}
518522
} finally {
519-
this.deferStatusUpdates = false;
520-
this.dispatchTypedEvent("status", new ConnectionStatusEvent(this.status));
523+
const previousStatus = this.deferredUpdatesPreviousStatus!;
524+
this.deferredUpdatesPreviousStatus = undefined;
525+
this.dispatchTypedEvent(
526+
"status",
527+
new ConnectionStatusEvent(this.status, previousStatus),
528+
);
521529
}
522530
}
523531

lib/device.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ export type FlashDataSource = (
213213
export type BoardVersion = "V1" | "V2";
214214

215215
export class ConnectionStatusEvent extends Event {
216-
constructor(public readonly status: ConnectionStatus) {
216+
constructor(
217+
public readonly status: ConnectionStatus,
218+
public readonly previousStatus: ConnectionStatus,
219+
) {
217220
super("status");
218221
}
219222
}

lib/usb-radio-bridge.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,14 @@ class MicrobitRadioBridgeConnectionImpl
234234
this.logging.log(v);
235235
}
236236

237-
private setStatus(status: ConnectionStatus) {
238-
this.status = status;
239-
this.log("Radio connection status " + status);
240-
this.dispatchTypedEvent("status", new ConnectionStatusEvent(status));
237+
private setStatus(newStatus: ConnectionStatus) {
238+
const previousStatus = this.status;
239+
this.status = newStatus;
240+
this.log("Radio connection status " + newStatus);
241+
this.dispatchTypedEvent(
242+
"status",
243+
new ConnectionStatusEvent(newStatus, previousStatus),
244+
);
241245
}
242246

243247
private statusFromDelegate(): ConnectionStatus {

lib/usb.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,13 @@ class MicrobitWebUSBConnectionImpl
422422
}
423423

424424
private setStatus(newStatus: ConnectionStatus) {
425+
const previousStatus = this.status;
425426
this.status = newStatus;
426427
this.log("USB connection status " + newStatus);
427-
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
428+
this.dispatchTypedEvent(
429+
"status",
430+
new ConnectionStatusEvent(newStatus, previousStatus),
431+
);
428432
}
429433

430434
private async withEnrichedErrors<T>(f: () => Promise<T>): Promise<T> {

0 commit comments

Comments
 (0)