207 lines
7.3 KiB
JavaScript
207 lines
7.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createServer } from "node:http";
|
|
import test from "node:test";
|
|
import { WebSocket } from "ws";
|
|
import { attachDiceSyncServer } from "./dice-sync.mjs";
|
|
|
|
const createInbox = (socket) => {
|
|
const messages = [];
|
|
const waiters = [];
|
|
socket.on("message", (raw) => {
|
|
const message = JSON.parse(raw.toString());
|
|
const waiterIndex = waiters.findIndex(
|
|
(waiter) => waiter.type === message.type && waiter.predicate(message),
|
|
);
|
|
if (waiterIndex >= 0) {
|
|
const [waiter] = waiters.splice(waiterIndex, 1);
|
|
clearTimeout(waiter.timeout);
|
|
waiter.resolve(message);
|
|
} else {
|
|
messages.push(message);
|
|
}
|
|
});
|
|
return {
|
|
next(type, predicate = () => true) {
|
|
const messageIndex = messages.findIndex(
|
|
(message) => message.type === type && predicate(message),
|
|
);
|
|
if (messageIndex >= 0)
|
|
return Promise.resolve(messages.splice(messageIndex, 1)[0]);
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(
|
|
() => reject(new Error(`Timed out waiting for ${type}`)),
|
|
2000,
|
|
);
|
|
waiters.push({ type, predicate, resolve, timeout });
|
|
});
|
|
},
|
|
};
|
|
};
|
|
|
|
const connect = (url) =>
|
|
new Promise((resolve, reject) => {
|
|
const socket = new WebSocket(url);
|
|
socket.once("open", () => resolve(socket));
|
|
socket.once("error", reject);
|
|
});
|
|
|
|
test("a host creates a room and broadcasts one authoritative roll", async () => {
|
|
const httpServer = createServer();
|
|
const { wss } = attachDiceSyncServer(httpServer);
|
|
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
|
|
const address = httpServer.address();
|
|
const url = `ws://127.0.0.1:${address.port}/sync`;
|
|
const host = await connect(url);
|
|
const guest = await connect(url);
|
|
const hostInbox = createInbox(host);
|
|
const guestInbox = createInbox(guest);
|
|
|
|
host.send(JSON.stringify({ type: "create" }));
|
|
const hostState = await hostInbox.next("room-state");
|
|
const tokenMessage = await hostInbox.next("host-token");
|
|
assert.equal(hostState.isHost, true);
|
|
assert.equal(tokenMessage.roomId, hostState.roomId);
|
|
|
|
guest.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
|
const guestState = await guestInbox.next("room-state");
|
|
assert.equal(guestState.isHost, false);
|
|
|
|
const hostRoll = hostInbox.next("rolled");
|
|
const guestRoll = guestInbox.next("rolled");
|
|
host.send(JSON.stringify({ type: "roll" }));
|
|
const [hostMessage, guestMessage] = await Promise.all([hostRoll, guestRoll]);
|
|
assert.deepEqual(guestMessage.roll, hostMessage.roll);
|
|
assert.equal(
|
|
hostMessage.roll.sum,
|
|
hostMessage.roll.dice[0] + hostMessage.roll.dice[1],
|
|
);
|
|
|
|
const forbidden = guestInbox.next("error");
|
|
guest.send(JSON.stringify({ type: "roll" }));
|
|
assert.match((await forbidden).message, /Only the room host/);
|
|
|
|
host.terminate();
|
|
guest.terminate();
|
|
await new Promise((resolve) => wss.close(resolve));
|
|
await new Promise((resolve) => httpServer.close(resolve));
|
|
});
|
|
|
|
test("level settings and re-rolls stay authoritative across a classroom", async () => {
|
|
const httpServer = createServer();
|
|
const { wss } = attachDiceSyncServer(httpServer);
|
|
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
|
|
const address = httpServer.address();
|
|
const url = `ws://127.0.0.1:${address.port}/sync`;
|
|
const host = await connect(url);
|
|
const guest = await connect(url);
|
|
const hostInbox = createInbox(host);
|
|
const guestInbox = createInbox(guest);
|
|
|
|
host.send(
|
|
JSON.stringify({
|
|
type: "create",
|
|
settings: { gridSize: 9, undoLimit: 0, rerollLimit: 2 },
|
|
}),
|
|
);
|
|
const hostState = await hostInbox.next("room-state");
|
|
await hostInbox.next("host-token");
|
|
assert.deepEqual(hostState.settings, {
|
|
gridSize: 9,
|
|
undoLimit: 0,
|
|
rerollLimit: 2,
|
|
});
|
|
assert.equal(hostState.rerollsUsed, 0);
|
|
|
|
guest.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
|
const guestState = await guestInbox.next("room-state");
|
|
assert.deepEqual(guestState.settings, hostState.settings);
|
|
|
|
const configuredSettings = {
|
|
gridSize: 8,
|
|
undoLimit: 4,
|
|
rerollLimit: 1,
|
|
};
|
|
const hostSettings = hostInbox.next("settings");
|
|
const guestSettings = guestInbox.next("settings");
|
|
host.send(
|
|
JSON.stringify({ type: "configure", settings: configuredSettings }),
|
|
);
|
|
const [hostSettingsMessage, guestSettingsMessage] = await Promise.all([
|
|
hostSettings,
|
|
guestSettings,
|
|
]);
|
|
assert.deepEqual(hostSettingsMessage.settings, configuredSettings);
|
|
assert.deepEqual(guestSettingsMessage.settings, configuredSettings);
|
|
|
|
const forbiddenConfiguration = guestInbox.next("error");
|
|
guest.send(
|
|
JSON.stringify({
|
|
type: "configure",
|
|
settings: { gridSize: 5, undoLimit: 3, rerollLimit: 3 },
|
|
}),
|
|
);
|
|
assert.match((await forbiddenConfiguration).message, /Only the room host/);
|
|
|
|
const firstHostRoll = hostInbox.next("rolled");
|
|
const firstGuestRoll = guestInbox.next("rolled");
|
|
host.send(JSON.stringify({ type: "roll" }));
|
|
const [hostRollMessage] = await Promise.all([firstHostRoll, firstGuestRoll]);
|
|
|
|
const hostReroll = hostInbox.next("rerolled");
|
|
const guestReroll = guestInbox.next("rerolled");
|
|
host.send(JSON.stringify({ type: "reroll" }));
|
|
const [hostRerollMessage, guestRerollMessage] = await Promise.all([
|
|
hostReroll,
|
|
guestReroll,
|
|
]);
|
|
assert.deepEqual(guestRerollMessage.roll, hostRerollMessage.roll);
|
|
assert.equal(hostRerollMessage.roll.id, hostRollMessage.roll.id);
|
|
assert.equal(hostRerollMessage.rerollsUsed, 1);
|
|
|
|
const exhausted = hostInbox.next("error");
|
|
host.send(JSON.stringify({ type: "reroll" }));
|
|
assert.match((await exhausted).message, /no re-rolls remaining/);
|
|
|
|
const firstHostReset = hostInbox.next("round-reset");
|
|
const firstGuestReset = guestInbox.next("round-reset");
|
|
host.send(JSON.stringify({ type: "reset" }));
|
|
const [resetMessage] = await Promise.all([firstHostReset, firstGuestReset]);
|
|
assert.equal(resetMessage.rerollsUsed, 0);
|
|
|
|
const secondHostRoll = hostInbox.next("rolled");
|
|
const secondGuestRoll = guestInbox.next("rolled");
|
|
host.send(JSON.stringify({ type: "roll" }));
|
|
await Promise.all([secondHostRoll, secondGuestRoll]);
|
|
|
|
const placedUpdate = hostInbox.next("leaderboard", (message) =>
|
|
message.leaderboard.some(
|
|
(player) => player.id === guestState.playerId && player.placed === 1,
|
|
),
|
|
);
|
|
guest.send(
|
|
JSON.stringify({ type: "progress", score: 8, placed: 1, finished: false }),
|
|
);
|
|
await placedUpdate;
|
|
|
|
const alreadyPlaced = hostInbox.next("error");
|
|
host.send(JSON.stringify({ type: "reroll" }));
|
|
assert.match((await alreadyPlaced).message, /already been placed/);
|
|
|
|
const secondHostReset = hostInbox.next("round-reset");
|
|
const secondGuestReset = guestInbox.next("round-reset");
|
|
host.send(JSON.stringify({ type: "reset" }));
|
|
await Promise.all([secondHostReset, secondGuestReset]);
|
|
|
|
const newcomer = await connect(url);
|
|
const newcomerInbox = createInbox(newcomer);
|
|
newcomer.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
|
const newcomerState = await newcomerInbox.next("room-state");
|
|
assert.deepEqual(newcomerState.settings, configuredSettings);
|
|
assert.equal(newcomerState.rerollsUsed, 0);
|
|
|
|
host.terminate();
|
|
guest.terminate();
|
|
newcomer.terminate();
|
|
await new Promise((resolve) => wss.close(resolve));
|
|
await new Promise((resolve) => httpServer.close(resolve));
|
|
});
|