Skip to content

Commit 1a34c4a

Browse files
committed
refactor(lib/testing): MockWebSocket.instances now returns AsyncGenerator
1 parent 793d039 commit 1a34c4a

File tree

4 files changed

+97
-45
lines changed

4 files changed

+97
-45
lines changed

lib/testing.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
type MessageEventData = string | ArrayBufferLike | Blob | ArrayBufferView;
22

33
export class MockWebSocket extends EventTarget implements WebSocket {
4-
/**
5-
* A list of all instances of MockWebSocket.
6-
* An instance is removed from this list when it is closed.
7-
*/
8-
static readonly instances = new Set<MockWebSocket>();
4+
/** An AsyncGenerator that yields WebSocket instances. */
5+
static async *instances() {
6+
for (;;) {
7+
if (this.#instances.length > 0) {
8+
yield this.#instances.shift()!;
9+
}
10+
await new Promise<void>((resolve) => {
11+
this.#pushed = resolve;
12+
});
13+
this.#pushed = undefined;
14+
}
15+
}
16+
static readonly #instances: MockWebSocket[] = [];
17+
static #pushed: (() => void) | undefined;
918

10-
constructor(url?: string | URL, protocols?: string | string[]) {
19+
constructor(
20+
url?: string | URL,
21+
protocols?: string | string[],
22+
isRemote = false,
23+
) {
1124
super();
1225
this.url = url?.toString() ?? "";
1326
this.protocol = protocols ? [...protocols].flat()[0] : "";
14-
MockWebSocket.instances.add(this);
1527
// Simulate async behavior of WebSocket as much as possible.
1628
queueMicrotask(() => {
1729
this.#readyState = 1;
1830
const ev = new Event("open");
1931
this.dispatchEvent(ev);
2032
this.onopen?.(ev);
2133
});
34+
if (!isRemote) {
35+
MockWebSocket.#instances.push(this);
36+
MockWebSocket.#pushed?.();
37+
}
2238
}
2339

2440
binaryType: "blob" | "arraybuffer" = "blob";
@@ -39,7 +55,7 @@ export class MockWebSocket extends EventTarget implements WebSocket {
3955

4056
get remote(): MockWebSocket {
4157
if (!this.#remote) {
42-
this.#remote = new MockWebSocket(this.url);
58+
this.#remote = new MockWebSocket(import.meta.url, undefined, true);
4359
this.#remote.#remote = this;
4460
}
4561
return this.#remote;
@@ -48,20 +64,20 @@ export class MockWebSocket extends EventTarget implements WebSocket {
4864

4965
close(code?: number, reason?: string): void {
5066
this.#readyState = 2;
67+
if (this.#remote) {
68+
this.#remote.#readyState = 2;
69+
}
5170
// Simulate async behavior of WebSocket as much as possible.
5271
queueMicrotask(() => {
72+
const ev = new CloseEvent("close", { code, reason });
5373
if (this.#remote) {
5474
this.#remote.#readyState = 3;
55-
const ev = new CloseEvent("close", { code, reason });
5675
this.#remote.dispatchEvent(ev);
5776
this.#remote.onclose?.(ev);
58-
MockWebSocket.instances.delete(this.#remote);
5977
}
6078
this.#readyState = 3;
61-
const ev = new CloseEvent("close", { code, reason });
6279
this.dispatchEvent(ev);
6380
this.onclose?.(ev);
64-
MockWebSocket.instances.delete(this);
6581
});
6682
}
6783

lib/testing_test.ts

+42-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeAll, describe, it } from "@std/testing/bdd";
2-
import { assert, assertEquals } from "@std/assert";
2+
import { assert, assertEquals, assertInstanceOf } from "@std/assert";
33
import { MockWebSocket } from "@lophus/lib/testing";
44

55
describe("MockWebSocket", () => {
@@ -21,8 +21,36 @@ describe("MockWebSocket", () => {
2121
});
2222

2323
describe("instances", () => {
24-
it("should return a set of instances", () => {
25-
assertEquals(MockWebSocket.instances.size, 2);
24+
it("should return an AsyncGenerator of WebSocket instances", () => {
25+
assertEquals(typeof MockWebSocket.instances().next, "function");
26+
});
27+
28+
describe("next", () => {
29+
let third: Promise<IteratorResult<MockWebSocket>>;
30+
31+
it("should return the first instance", async () => {
32+
const { value, done } = await MockWebSocket.instances().next();
33+
assert(!done);
34+
assertEquals(value.url, "");
35+
});
36+
37+
it("should return the second instance", async () => {
38+
const { value, done } = await MockWebSocket.instances().next();
39+
assert(!done);
40+
assertEquals(value.url, "wss://localhost:8080");
41+
});
42+
43+
it("should return a promise for the next instance", () => {
44+
third = MockWebSocket.instances().next();
45+
assertInstanceOf(third, Promise);
46+
});
47+
48+
it("should return the third instance when created", async () => {
49+
new MockWebSocket("wss://localhost:8081");
50+
const { value, done } = await third;
51+
assert(!done);
52+
assertEquals(value.url, "wss://localhost:8081");
53+
});
2654
});
2755
});
2856

@@ -35,27 +63,27 @@ describe("MockWebSocket", () => {
3563
describe("remote", () => {
3664
it("should return a remote instance", () => {
3765
assert(ws.remote);
38-
assertEquals(ws.remote.url, ws.url);
66+
assert(ws.remote.url.startsWith("file:"));
3967
assertEquals(ws.remote.remote, ws);
4068
});
4169
});
4270

4371
describe("send", () => {
4472
it("should send a message to the remote WebSocket", async () => {
45-
const promise = new Promise<true>((resolve) => {
46-
ws.remote.addEventListener("message", () => resolve(true));
73+
const promise = new Promise((resolve) => {
74+
ws.remote.addEventListener("message", resolve);
4775
});
4876
ws.send("test");
4977
assert(await promise);
5078
});
5179
});
5280

5381
describe("close", () => {
54-
let remote_closed: Promise<true>;
82+
let remote_closed: Promise<unknown>;
5583

5684
beforeAll(() => {
57-
remote_closed = new Promise<true>((resolve) => {
58-
ws.remote.addEventListener("close", () => resolve(true));
85+
remote_closed = new Promise((resolve) => {
86+
ws.remote.addEventListener("close", resolve);
5987
});
6088
});
6189

@@ -64,16 +92,14 @@ describe("MockWebSocket", () => {
6492
ws.addEventListener("close", () => resolve(true));
6593
});
6694
ws.close();
67-
assert(await closed);
95+
assertEquals(ws.readyState, WebSocket.CLOSING);
96+
await closed;
6897
assertEquals(ws.readyState, WebSocket.CLOSED);
6998
});
7099

71-
it("should close the remote WebSocket", () => {
72-
assertEquals(ws.remote.readyState, WebSocket.CLOSED);
73-
});
74-
75-
it("should trigger the onclose event on the remote WebSocket", async () => {
76-
assert(await remote_closed);
100+
it("should close the remote WebSocket", async () => {
101+
await remote_closed;
102+
assert(ws.remote.readyState === WebSocket.CLOSED);
77103
});
78104
});
79105
});

lib/websockets_test.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("LazyWebSocket", () => {
2828

2929
it("should not open a WebSocket when an event listener is added", () => {
3030
opened = new Promise((resolve) => {
31-
lazy.addEventListener("open", () => resolve(true));
31+
lazy.addEventListener("open", () => resolve(true), { once: true });
3232
});
3333
assertEquals(lazy.readyState, WebSocket.CLOSED);
3434
});
@@ -46,14 +46,15 @@ describe("LazyWebSocket", () => {
4646
const errored = new Promise((resolve) => {
4747
lazy.addEventListener("error", resolve);
4848
});
49-
socket = MockWebSocket.instances.values().next().value;
49+
const { value } = await MockWebSocket.instances().next();
50+
socket = value!;
5051
socket.dispatchEvent(new Event("error"));
5152
await errored;
5253
});
5354

5455
it("should trigger the onmessage event when receives a message", async () => {
5556
const messaged = new Promise((resolve) => {
56-
lazy.addEventListener("message", resolve);
57+
lazy.addEventListener("message", resolve, { once: true });
5758
});
5859
server = socket.remote;
5960
server.send("test");
@@ -63,12 +64,21 @@ describe("LazyWebSocket", () => {
6364
it("should trigger the onclose event when the WebSocket is closed", async () => {
6465
await lazy.ready();
6566
const closed = new Promise((resolve) => {
66-
lazy.addEventListener("close", resolve);
67+
lazy.addEventListener("close", resolve, { once: true });
6768
});
6869
server.close();
6970
await closed;
7071
});
7172

73+
it("should reregister the event listeners when the WebSocket is recreated", async () => {
74+
const closed = new Promise((resolve) => {
75+
lazy.addEventListener("close", resolve, { once: true });
76+
});
77+
await lazy.ready();
78+
server.close();
79+
await closed;
80+
});
81+
7282
it("should close the WebSocket when the LazyWebSocket is closed", async () => {
7383
await lazy.close();
7484
assertEquals(lazy.readyState, WebSocket.CLOSED);

nips/01/relays_test.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import {
1515
} from "@lophus/core/relays";
1616
import { Relay } from "../relays.ts";
1717

18-
function getRemoteSocket() {
19-
return MockWebSocket.instances.values().next().value.remote;
18+
async function getRemoteSocket() {
19+
const { value } = await MockWebSocket.instances().next();
20+
return value!.remote;
2021
}
2122

2223
describe("Relay (NIP-01)", () => {
2324
const url = "wss://localhost:8080";
2425
let relay: Relay;
26+
let remote: MockWebSocket;
2527
let sub_0: ReadableStream<NostrEvent<0>>;
2628
let sub_1: ReadableStream<NostrEvent<1>>;
2729

@@ -47,9 +49,9 @@ describe("Relay (NIP-01)", () => {
4749

4850
it("should receive text notes", async () => {
4951
const reader = sub_1.getReader();
50-
const read = reader.read();
51-
getRemoteSocket().send(JSON.stringify(["EVENT", "test-1", { kind: 1 }]));
52-
const { value, done } = await read;
52+
remote = await getRemoteSocket();
53+
remote.send(JSON.stringify(["EVENT", "test-1", { kind: 1 }]));
54+
const { value, done } = await reader.read();
5355
assert(!done);
5456
assertEquals(value.kind, 1);
5557
reader.releaseLock();
@@ -63,7 +65,6 @@ describe("Relay (NIP-01)", () => {
6365
it("should recieve metas and notes simultaneously", async () => {
6466
const reader_0 = sub_0.getReader();
6567
const reader_1 = sub_1.getReader();
66-
const remote = getRemoteSocket();
6768
remote.send(JSON.stringify(["EVENT", "test-0", { kind: 0 }]));
6869
remote.send(JSON.stringify(["EVENT", "test-1", { kind: 1 }]));
6970
const [{ value: value_0 }, { value: value_1 }] = await Promise.all([
@@ -79,7 +80,7 @@ describe("Relay (NIP-01)", () => {
7980
});
8081

8182
it("should close a subscription with an error when receiving a CLOSED message", async () => {
82-
getRemoteSocket().send(JSON.stringify(
83+
remote.send(JSON.stringify(
8384
[
8485
"CLOSED",
8586
"test-1" as SubscriptionId,
@@ -99,14 +100,15 @@ describe("Relay (NIP-01)", () => {
99100

100101
it("should reconnect if connection is closed while waiting for an event", async () => {
101102
const reader = sub_0.getReader();
102-
const read = reader.read();
103-
getRemoteSocket().close();
103+
const read = reader.read(); // wait for an event
104+
remote.close();
104105
const reconnected = new Promise<true>((resolve) => {
105106
relay.ws.addEventListener("open", () => resolve(true));
106107
});
107108
assert(await reconnected);
108109
// We must use a new instance of MockWebSocket.
109-
getRemoteSocket().send(JSON.stringify(["EVENT", "test-0", { kind: 0 }]));
110+
remote = await getRemoteSocket();
111+
remote.send(JSON.stringify(["EVENT", "test-0", { kind: 0 }]));
110112
const { value, done } = await read;
111113
assert(!done);
112114
assertEquals(value.kind, 0);
@@ -115,7 +117,6 @@ describe("Relay (NIP-01)", () => {
115117

116118
it("should publish an event and recieve an accepting OK message", async () => {
117119
const eid = "test-true" as EventId;
118-
const remote = getRemoteSocket();
119120
const arrived = new Promise<true>((resolve) => {
120121
remote.addEventListener(
121122
"message",
@@ -143,7 +144,6 @@ describe("Relay (NIP-01)", () => {
143144
const eid = "test-false" as EventId;
144145
// deno-fmt-ignore
145146
const msg = ["OK", eid, false, "error: test"] satisfies RelayToClientMessage<"OK">
146-
const remote = getRemoteSocket();
147147
const arrived = new Promise<true>((resolve) => {
148148
remote.addEventListener(
149149
"message",
@@ -174,7 +174,7 @@ describe("Relay (NIP-01)", () => {
174174
const event = { id: "test-close" as EventId, kind: 1 };
175175
// deno-lint-ignore no-explicit-any
176176
const published = relay.publish(event as any).catch((e) => e);
177-
getRemoteSocket().close();
177+
remote.close();
178178
try {
179179
await published;
180180
} catch (e) {

0 commit comments

Comments
 (0)