125 lines
2.9 KiB
JavaScript
125 lines
2.9 KiB
JavaScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import test from "node:test";
|
||
|
|
import { HostTabSync } from "../src/host/tab-sync.ts";
|
||
|
|
|
||
|
|
class FakeBus {
|
||
|
|
channels = new Set();
|
||
|
|
messages = [];
|
||
|
|
|
||
|
|
createChannel() {
|
||
|
|
const channel = new FakeChannel(this);
|
||
|
|
this.channels.add(channel);
|
||
|
|
return channel;
|
||
|
|
}
|
||
|
|
|
||
|
|
deliver(sender, message) {
|
||
|
|
const copy = structuredClone(message);
|
||
|
|
this.messages.push(copy);
|
||
|
|
for (const channel of this.channels) {
|
||
|
|
if (channel !== sender) channel.receive(copy);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
replay(message) {
|
||
|
|
for (const channel of this.channels) channel.receive(structuredClone(message));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class FakeChannel {
|
||
|
|
listeners = new Set();
|
||
|
|
|
||
|
|
constructor(bus) {
|
||
|
|
this.bus = bus;
|
||
|
|
}
|
||
|
|
|
||
|
|
postMessage(message) {
|
||
|
|
this.bus.deliver(this, message);
|
||
|
|
}
|
||
|
|
|
||
|
|
addEventListener(type, listener) {
|
||
|
|
if (type === "message") this.listeners.add(listener);
|
||
|
|
}
|
||
|
|
|
||
|
|
removeEventListener(type, listener) {
|
||
|
|
if (type === "message") this.listeners.delete(listener);
|
||
|
|
}
|
||
|
|
|
||
|
|
receive(data) {
|
||
|
|
for (const listener of this.listeners) listener({ data });
|
||
|
|
}
|
||
|
|
|
||
|
|
close() {
|
||
|
|
this.bus.channels.delete(this);
|
||
|
|
this.listeners.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
test("new tabs request and receive the current Host state", () => {
|
||
|
|
const bus = new FakeBus();
|
||
|
|
const firstState = hostState("http://runtime-a.test");
|
||
|
|
const received = [];
|
||
|
|
const first = new HostTabSync({
|
||
|
|
tabId: "tab-a",
|
||
|
|
channel: bus.createChannel(),
|
||
|
|
getState: () => firstState,
|
||
|
|
applyState: () => {},
|
||
|
|
});
|
||
|
|
const second = new HostTabSync({
|
||
|
|
tabId: "tab-b",
|
||
|
|
channel: bus.createChannel(),
|
||
|
|
getState: () => hostState("http://runtime-b.test"),
|
||
|
|
applyState: (state) => received.push(state),
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.deepEqual(received, [firstState]);
|
||
|
|
first.close();
|
||
|
|
second.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("publishes Host state but rejects self, stale and malformed messages", () => {
|
||
|
|
const bus = new FakeBus();
|
||
|
|
const received = [];
|
||
|
|
const first = new HostTabSync({
|
||
|
|
tabId: "tab-a",
|
||
|
|
channel: bus.createChannel(),
|
||
|
|
getState: () => hostState(""),
|
||
|
|
applyState: (state) => received.push(state),
|
||
|
|
});
|
||
|
|
const second = new HostTabSync({
|
||
|
|
tabId: "tab-b",
|
||
|
|
channel: bus.createChannel(),
|
||
|
|
getState: () => hostState(""),
|
||
|
|
applyState: () => {},
|
||
|
|
});
|
||
|
|
received.length = 0;
|
||
|
|
|
||
|
|
const update = hostState("http://shared.test");
|
||
|
|
second.publish(update);
|
||
|
|
assert.deepEqual(received, [update]);
|
||
|
|
|
||
|
|
const stateMessage = bus.messages.findLast(
|
||
|
|
(message) => message.type === "state" && message.sender === "tab-b",
|
||
|
|
);
|
||
|
|
bus.replay(stateMessage);
|
||
|
|
bus.replay({
|
||
|
|
...stateMessage,
|
||
|
|
sequence: stateMessage.sequence + 1,
|
||
|
|
state: { ...stateMessage.state, connection: "invalid" },
|
||
|
|
});
|
||
|
|
assert.deepEqual(received, [update]);
|
||
|
|
|
||
|
|
first.close();
|
||
|
|
second.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
function hostState(apiBase) {
|
||
|
|
return {
|
||
|
|
snapshot: null,
|
||
|
|
connection: "online",
|
||
|
|
apiBase,
|
||
|
|
runtimeAction: null,
|
||
|
|
serviceAction: null,
|
||
|
|
deploymentAction: null,
|
||
|
|
};
|
||
|
|
}
|