test(timeout): copri startTimeout/stopTimeout e il timer server-side dei 30s

Unit: attivazione con squadra/video/inizio, rifiuto di squadre non valide,
azzeramento completo con stopTimeout. Integration (fake timers): startedAt
iniettato dal server, auto-chiusura a 30s con broadcast e persistenza,
annullamento del timer su stop manuale e riarmo su nuovo avvio.
This commit is contained in:
2026-07-03 10:21:02 +02:00
parent c4599a2722
commit 58554c3c61
2 changed files with 159 additions and 7 deletions
+111
View File
@@ -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 // BROADCAST MULTI-CLIENT
// ============================================= // =============================================
+48 -7
View File
@@ -80,8 +80,11 @@ describe('Game Logic (gameState.js)', () => {
expect(state.visuStriscia).toBe(true) 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.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) 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) // TIME OUT (startTimeout, stopTimeout)
const s2 = applyAction(s1, { type: 'toggleTimeout' }) // =============================================
expect(s2.timeout).toBe(false) 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)
}) })
}) })