interactives/server/dice-sync.test.mjs
Neeldhara Misra e6fa86519c
Some checks failed
CI / validate (push) Has been cancelled
Add Akkam Bakkam classroom game
2026-07-20 17:23:12 +05:30

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));
});