diff --git a/tests/integration/websocket.test.js b/tests/integration/websocket.test.js index 21337a6..5edf11a 100644 --- a/tests/integration/websocket.test.js +++ b/tests/integration/websocket.test.js @@ -164,6 +164,117 @@ describe('WebSocket Integration (websocket-handler.js)', () => { }) }) + // ============================================= + // TIME OUT (timer server-side) + // ============================================= + describe('Time out', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + function inviaStartTimeout(controller, overrides = {}) { + controller.emit('message', JSON.stringify({ + type: 'action', + action: { type: 'startTimeout', team: 'home', video: 'spot.mp4', ...overrides } + })) + } + + it('dovrebbe iniettare startedAt lato server (ignorando quello del client)', () => { + vi.setSystemTime(50000) + const controller = connectAndRegister(wss, 'controller') + + inviaStartTimeout(controller, { startedAt: 12345 }) + + const state = handler.getState() + expect(state.timeout).toBe(true) + expect(state.timeoutStartedAt).toBe(50000) + }) + + it('dovrebbe chiudere automaticamente il time out dopo 30 secondi', () => { + const controller = connectAndRegister(wss, 'controller') + const display = connectAndRegister(wss, 'display') + + inviaStartTimeout(controller) + expect(handler.getState().timeout).toBe(true) + + vi.advanceTimersByTime(29999) + expect(handler.getState().timeout).toBe(true) + + vi.advanceTimersByTime(1) + const state = handler.getState() + expect(state.timeout).toBe(false) + expect(state.timeoutTeam).toBe(null) + expect(state.timeoutVideo).toBe(null) + expect(state.timeoutStartedAt).toBe(null) + + // La chiusura automatica viene broadcastata ai client + const msg = lastSent(display) + expect(msg.type).toBe('state') + expect(msg.state.timeout).toBe(false) + }) + + it('la chiusura automatica dovrebbe persistere lo stato (onStateChange)', () => { + const onStateChange = vi.fn() + wss = new MockWebSocketServer() + handler = setupWebSocketHandler(wss, { onStateChange }) + const controller = connectAndRegister(wss, 'controller') + + inviaStartTimeout(controller) + onStateChange.mockClear() + + vi.advanceTimersByTime(30000) + expect(onStateChange).toHaveBeenCalledTimes(1) + expect(onStateChange.mock.calls[0][0].timeout).toBe(false) + }) + + it('stopTimeout manuale dovrebbe annullare il timer pendente', () => { + const controller = connectAndRegister(wss, 'controller') + + inviaStartTimeout(controller) + controller.emit('message', JSON.stringify({ + type: 'action', + action: { type: 'stopTimeout' } + })) + expect(handler.getState().timeout).toBe(false) + controller.send.mockClear() + + // Allo scadere dei 30s non deve partire nessun broadcast fantasma + vi.advanceTimersByTime(30000) + expect(controller.send).not.toHaveBeenCalled() + }) + + it('un nuovo startTimeout dovrebbe riarmare il timer da capo', () => { + const controller = connectAndRegister(wss, 'controller') + + inviaStartTimeout(controller) + vi.advanceTimersByTime(20000) + + // Riavvio: il timer riparte da zero + inviaStartTimeout(controller, { team: 'guest' }) + vi.advanceTimersByTime(20000) + expect(handler.getState().timeout).toBe(true) + expect(handler.getState().timeoutTeam).toBe('guest') + + vi.advanceTimersByTime(10000) + expect(handler.getState().timeout).toBe(false) + }) + + it('startTimeout con squadra non valida non dovrebbe armare il timer', () => { + const controller = connectAndRegister(wss, 'controller') + + inviaStartTimeout(controller, { team: 'arbitro' }) + expect(handler.getState().timeout).toBe(false) + controller.send.mockClear() + + vi.advanceTimersByTime(30000) + expect(controller.send).not.toHaveBeenCalled() + }) + }) + // ============================================= // BROADCAST MULTI-CLIENT // ============================================= diff --git a/tests/unit/gameState.test.js b/tests/unit/gameState.test.js index 6ef3818..147fa1d 100644 --- a/tests/unit/gameState.test.js +++ b/tests/unit/gameState.test.js @@ -80,8 +80,11 @@ describe('Game Logic (gameState.js)', () => { expect(state.visuStriscia).toBe(true) }) - it('dovrebbe avere timeout false', () => { + it('dovrebbe avere timeout false e campi timeout nulli', () => { expect(state.timeout).toBe(false) + expect(state.timeoutTeam).toBe(null) + expect(state.timeoutVideo).toBe(null) + expect(state.timeoutStartedAt).toBe(null) }) }) @@ -484,12 +487,50 @@ describe('Game Logic (gameState.js)', () => { expect(s.order).toBe(false) }) - it('toggleTimeout: false → true → false', () => { - expect(state.timeout).toBe(false) - const s1 = applyAction(state, { type: 'toggleTimeout' }) - expect(s1.timeout).toBe(true) - const s2 = applyAction(s1, { type: 'toggleTimeout' }) - expect(s2.timeout).toBe(false) + }) + + // ============================================= + // TIME OUT (startTimeout, stopTimeout) + // ============================================= + describe('Time out', () => { + it('startTimeout dovrebbe attivare il time out con squadra, video e inizio', () => { + const s = applyAction(state, { type: 'startTimeout', team: 'home', video: 'spot.mp4', startedAt: 1000 }) + expect(s.timeout).toBe(true) + expect(s.timeoutTeam).toBe('home') + expect(s.timeoutVideo).toBe('spot.mp4') + expect(s.timeoutStartedAt).toBe(1000) + }) + + it('startTimeout senza video dovrebbe lasciare timeoutVideo null', () => { + const s = applyAction(state, { type: 'startTimeout', team: 'guest', startedAt: 1000 }) + expect(s.timeout).toBe(true) + expect(s.timeoutTeam).toBe('guest') + expect(s.timeoutVideo).toBe(null) + }) + + it('startTimeout senza squadra valida dovrebbe essere ignorato', () => { + for (const team of [undefined, null, 'arbitro', '']) { + const s = applyAction(state, { type: 'startTimeout', team, video: 'spot.mp4', startedAt: 1000 }) + expect(s.timeout).toBe(false) + expect(s.timeoutTeam).toBe(null) + expect(s.timeoutVideo).toBe(null) + expect(s.timeoutStartedAt).toBe(null) + } + }) + + it('stopTimeout dovrebbe azzerare tutti i campi del time out', () => { + const attivo = applyAction(state, { type: 'startTimeout', team: 'home', video: 'spot.mp4', startedAt: 1000 }) + const s = applyAction(attivo, { type: 'stopTimeout' }) + expect(s.timeout).toBe(false) + expect(s.timeoutTeam).toBe(null) + expect(s.timeoutVideo).toBe(null) + expect(s.timeoutStartedAt).toBe(null) + }) + + it('stopTimeout su time out già inattivo non dovrebbe avere effetti', () => { + const s = applyAction(state, { type: 'stopTimeout' }) + expect(s.timeout).toBe(false) + expect(s.timeoutTeam).toBe(null) }) })