// Color Together mode socket test. const { io } = require("socket.io-client"); const URL = "http://localhost:3000"; const TIMEOUT_MS = 20000; const results = []; let exitCode = 0; const pass = (n) => { results.push({ name: n, status: "pass" }); console.log("PASS:", n); }; const fail = (n, e) => { results.push({ name: n, status: "fail", error: String(e) }); console.log("FAIL:", n, "-", e); exitCode = 1; }; function waitFor(socket, event, predicate = () => true, timeout = 6000) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error(`timeout waiting for ${event}`)), timeout); const handler = (data) => { if (predicate(data)) { clearTimeout(t); socket.off(event, handler); resolve(data); } }; socket.on(event, handler); }); } function emitAck(s, event, payload) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error(`ack timeout for ${event}`)), 5000); s.emit(event, payload, (resp) => { clearTimeout(t); resolve(resp); }); }); } const killer = setTimeout(() => { console.log("FAIL: Global timeout"); process.exit(1); }, TIMEOUT_MS); killer.unref(); (async () => { let a, b, c; try { a = io(URL, { transports: ["websocket"], forceNew: true }); b = io(URL, { transports: ["websocket"], forceNew: true }); await Promise.all([ new Promise((r) => a.on("connect", r)), new Promise((r) => b.on("connect", r)), ]); pass("color: clients A,B connected"); const create = await emitAck(a, "room:create", { nickname: "Alice", avatar: "🐱", mode: "color", settings: { canvasType: "blank" }, }); if (!create || !create.ok) throw new Error("create failed: " + JSON.stringify(create)); const code = create.code; pass("color: room created, code=" + code); const j = await emitAck(b, "room:join", { code, nickname: "Bob", avatar: "🐶" }); if (!j || !j.ok) throw new Error("join failed"); pass("color: B joined"); // listen FIRST, then trigger const stateAfterStartP = waitFor(a, "room:state", (s) => s.color !== null && s.phase === "playing", 4000); const start = await emitAck(a, "game:start", {}); if (!start || !start.ok) throw new Error("game:start failed: " + JSON.stringify(start)); pass("color: game:start ok"); const stateAfterStart = await stateAfterStartP; pass("color: room transitioned to playing with color state"); // A emits color:stroke; B should receive color:strokeBroadcast const strokePayload = { points: [{ x: 10, y: 10 }, { x: 20, y: 20 }], color: "#FF0000", size: 5, tool: "brush", }; const bRecv = waitFor(b, "color:strokeBroadcast", (d) => d && d.color === "#FF0000", 4000); a.emit("color:stroke", strokePayload); const got = await bRecv; if (!got || !Array.isArray(got.points) || got.points.length !== 2) throw new Error("stroke broadcast malformed: " + JSON.stringify(got)); pass("color: B received color:strokeBroadcast with same payload"); // C joins mid-game and should receive color:state with the existing stroke c = io(URL, { transports: ["websocket"], forceNew: true }); await new Promise((r) => c.on("connect", r)); const cStateP = waitFor(c, "color:state", (s) => s && Array.isArray(s.strokes), 5000); const j2 = await emitAck(c, "room:join", { code, nickname: "Carol", avatar: "🦊" }); if (!j2 || !j2.ok) throw new Error("C join failed"); pass("color: C joined mid-game"); const cState = await cStateP; if (!cState.strokes || cState.strokes.length < 1) throw new Error("color:state to new joiner missing strokes: " + JSON.stringify(cState)); pass("color: new joiner C received color:state with " + cState.strokes.length + " stroke(s)"); } catch (e) { fail("color flow", e && e.message ? e.message : String(e)); } finally { try { a && a.disconnect(); } catch (_) {} try { b && b.disconnect(); } catch (_) {} try { c && c.disconnect(); } catch (_) {} clearTimeout(killer); console.log("__RESULTS__ " + JSON.stringify(results)); setTimeout(() => process.exit(exitCode), 200); } })();