// E2E socket flow test for SKRIBBL mode. const { io } = require("socket.io-client"); const URL = "http://localhost:3000"; const TIMEOUT_MS = 25000; const results = []; let exitCode = 0; function pass(name) { results.push({ name, status: "pass" }); console.log("PASS:", name); } function fail(name, err) { results.push({ name, status: "fail", error: String(err) }); console.log("FAIL:", name, "-", err); exitCode = 1; } const wait = (ms) => new Promise(r => setTimeout(r, ms)); function waitFor(socket, event, predicate = () => true, timeout = 8000) { 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(socket, event, payload) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error(`ack timeout for ${event}`)), 5000); socket.emit(event, payload, (resp) => { clearTimeout(t); resolve(resp); }); }); } const killer = setTimeout(() => { console.log("FAIL: Global timeout reached"); process.exit(1); }, TIMEOUT_MS); killer.unref(); (async () => { let a, b; 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("clients A and B connected"); // 1. A creates a skribbl room const createResp = await emitAck(a, "room:create", { nickname: "Alice", avatar: "🐱", mode: "skribbl", settings: { rounds: 2, drawTimeSec: 60, language: "en" }, }); if (!createResp || !createResp.ok || !createResp.code) throw new Error("create failed: " + JSON.stringify(createResp)); const code = createResp.code; pass("A: room:create -> ok, code=" + code); // capture state from B's join const aStateP = waitFor(a, "room:state", (s) => s.players.length >= 2, 5000); const bStateP = waitFor(b, "room:state", (s) => s.players.length >= 2, 5000); // 2. B joins const joinResp = await emitAck(b, "room:join", { code, nickname: "Bob", avatar: "🐶" }); if (!joinResp || !joinResp.ok) throw new Error("join failed: " + JSON.stringify(joinResp)); pass("B: room:join -> ok"); // 3. Both clients receive room:state with both players const aState = await aStateP; const bState = await bStateP; const aHasBoth = aState.players.find(p => p.nickname === "Alice") && aState.players.find(p => p.nickname === "Bob"); const bHasBoth = bState.players.find(p => p.nickname === "Alice") && bState.players.find(p => p.nickname === "Bob"); if (!aHasBoth || !bHasBoth) throw new Error("room:state missing players"); pass("Both clients receive room:state with both players"); // 4. Chat send/receive const aChatP = waitFor(a, "chat:msg", (m) => m.text === "hello" && m.fromName === "Alice", 5000); const bChatP = waitFor(b, "chat:msg", (m) => m.text === "hello" && m.fromName === "Alice", 5000); a.emit("chat:send", { text: "hello" }); await Promise.all([aChatP, bChatP]); pass("chat:send broadcasts to both clients"); // 5. Start the game // Drawer (A is host & first in order) gets skribbl:wordChoices const choicesP = waitFor(a, "skribbl:wordChoices", () => true, 5000); const startResp = await emitAck(a, "game:start", {}); if (!startResp || !startResp.ok) throw new Error("start failed: " + JSON.stringify(startResp)); pass("game:start ack ok"); const choices = await choicesP; if (!Array.isArray(choices) || choices.length < 1) throw new Error("invalid choices"); pass("drawer received skribbl:wordChoices: " + JSON.stringify(choices)); // 6. Drawer picks a word const pickedWord = choices[0]; // Listen for roundStart on B const bRoundStartP = waitFor(b, "skribbl:roundStart", () => true, 5000); const aRoundStartP = waitFor(a, "skribbl:roundStart", () => true, 5000); const pickResp = await emitAck(a, "skribbl:pickWord", { word: pickedWord }); if (!pickResp || !pickResp.ok) throw new Error("pickWord failed: " + JSON.stringify(pickResp)); pass("skribbl:pickWord ack ok, word=" + pickedWord); const aRound = await aRoundStartP; const bRound = await bRoundStartP; if (!aRound || !bRound) throw new Error("roundStart missing"); pass("both clients received skribbl:roundStart"); // 7. Drawer emits strokes — other client receives them const strokeP = waitFor(b, "skribbl:stroke", (s) => s && s.color === "#000", 3000); a.emit("skribbl:stroke", { x: 10, y: 10, prevX: 5, prevY: 5, color: "#000", size: 4 }); a.emit("skribbl:stroke", { x: 20, y: 20, prevX: 10, prevY: 10, color: "#000", size: 4 }); const strokeReceived = await strokeP; if (!strokeReceived) throw new Error("no stroke received on B"); pass("B received skribbl:stroke from A"); // 8. B guesses correctly const aCorrectP = waitFor(a, "chat:msg", (m) => m.kind === "correct", 5000); const bCorrectP = waitFor(b, "chat:msg", (m) => m.kind === "correct", 5000); b.emit("chat:send", { text: pickedWord }); await Promise.all([aCorrectP, bCorrectP]); pass("correct guess produces 'correct' chat message on both clients"); // verify state shows score updated const stateAfterP = waitFor(a, "room:state", (s) => { const bob = s.players.find(p => p.nickname === "Bob"); return bob && bob.score > 0; }, 5000); const stateAfter = await stateAfterP; pass("room:state shows Bob score > 0 after correct guess: " + stateAfter.players.find(p=>p.nickname==="Bob").score); // 9. Wait for round end const roundEndP = waitFor(b, "skribbl:roundEnd", () => true, 8000); const roundEnd = await roundEndP; if (!roundEnd || !roundEnd.word) throw new Error("roundEnd missing word"); pass("skribbl:roundEnd fired, word=" + roundEnd.word); } catch (e) { fail("flow", e && e.message ? e.message : String(e)); } finally { try { a && a.disconnect(); } catch (_) {} try { b && b.disconnect(); } catch (_) {} clearTimeout(killer); // Print summary as JSON for parser console.log("__RESULTS__ " + JSON.stringify(results)); setTimeout(() => process.exit(exitCode), 200); } })();