// Test 2: hints, timer ticks, quick_join, mid-game joiner snapshot const WebSocket = require('ws'); const URL = 'ws://localhost:3000'; class Client { constructor(name) { this.name = name; this.msgs = []; this.ws = new WebSocket(URL); this.ws.on('open', () => this.send({ t: 'hello', name, avatar: { color: 2, eyes: 2, mouth: 2, extra: 2 } })); this.ws.on('message', (raw) => { const m = JSON.parse(raw.toString()); this.msgs.push(m); }); } send(m) { this.ws.send(JSON.stringify(m)); } last(t) { return [...this.msgs].reverse().find(m => m.t === t); } all(t) { return this.msgs.filter(m => m.t === t); } close() { this.ws.close(); } } const wait = (ms) => new Promise(r => setTimeout(r, ms)); async function waitFor(fn, what, timeout = 20000) { const t0 = Date.now(); while (Date.now() - t0 < timeout) { const v = fn(); if (v) return v; await wait(50); } throw new Error('timeout: ' + what); } (async () => { const a = new Client('P1'); await waitFor(() => a.last('welcome'), 'welcome'); a.send({ t: 'quick_join' }); const ja = await waitFor(() => a.last('joined'), 'joined a'); console.log('✔ quick_join created/joined public room:', ja.room.code, '| public:', ja.room.settings.isPublic); const b = new Client('P2'); await waitFor(() => b.last('welcome'), 'welcome b'); b.send({ t: 'quick_join' }); const jb = await waitFor(() => b.last('joined'), 'joined b'); console.log('✔ second quick_join landed in same room:', jb.room.code === ja.room.code); // public list shows it b.send({ t: 'list_rooms' }); await waitFor(() => b.last('room_list'), 'room_list'); const listed = b.last('room_list').rooms.find(r => r.code === ja.room.code); console.log('✔ public room listed:', !!listed, `(${listed.players}/${listed.maxPlayers})`); a.send({ t: 'set_settings', settings: { drawTime: 30, rounds: 2 } }); await wait(200); a.send({ t: 'start_game' }); const cw = await waitFor(() => a.last('choose_words'), 'choose_words'); a.send({ t: 'pick_word', word: cw.words[0] }); const ds = await waitFor(() => b.last('draw_start'), 'draw_start'); console.log('✔ word:', cw.words[0], '| initial mask:', ds.mask); // mid-game joiner gets snapshot with ops + mask const c = new Client('P3'); await waitFor(() => c.last('welcome'), 'welcome c'); c.send({ t: 'join_room', code: ja.room.code }); const jc = await waitFor(() => c.last('joined'), 'joined c'); console.log('✔ mid-game join snapshot: phase:', jc.room.phase, '| mask:', jc.room.mask === ds.mask, '| timeLeft>0:', jc.room.timeLeft > 0); // timer broadcasts await waitFor(() => c.all('timer').length >= 3, 'timer ticks', 6000); console.log('✔ timer broadcasts ticking:', c.all('timer').slice(-1)[0].timeLeft <= 30); // hint at ~45% of 30s = ~13.5s const hint = await waitFor(() => b.last('hint'), 'hint', 20000); console.log('✔ hint revealed:', hint.mask, '| differs from start:', hint.mask !== ds.mask); a.close(); b.close(); c.close(); console.log('\nTEST 2 DONE'); process.exit(0); })().catch(e => { console.error('TEST 2 FAILED:', e.message); process.exit(1); });