// @vitest-environment happy-dom import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { mount } from '@vue/test-utils' import { createWsMixin } from '../../src/wsMixin.js' import { createInitialState } from '../../src/gameState.js' // WebSocket mock controllabile: i gestori (onopen/onmessage/onclose) vengono // assegnati dal mixin e li invochiamo manualmente nei test. class MockWebSocket { static CONNECTING = 0 static OPEN = 1 static CLOSING = 2 static CLOSED = 3 constructor(url) { this.url = url this.readyState = MockWebSocket.CONNECTING this.send = vi.fn() this.close = vi.fn() MockWebSocket.instances.push(this) } } MockWebSocket.instances = [] vi.stubGlobal('WebSocket', MockWebSocket) // Monta un componente che usa il mixin. `extra` permette di aggiungere hook. function mountWith(role = 'controller', extra = {}) { const Comp = { mixins: [createWsMixin(role)], template: '
', ...extra, } return mount(Comp) } function ultimaWs() { return MockWebSocket.instances.at(-1) } describe('createWsMixin (wsMixin.js)', () => { beforeEach(() => { MockWebSocket.instances = [] }) afterEach(() => { vi.restoreAllMocks() }) describe('computed derivati', () => { it('punt/servHome/set delegano alle funzioni pure sulla striscia', async () => { const wrapper = mountWith() wrapper.vm.state.sp.striscia = [ { serv: 'h', ris: 'h', vinc: 'h' }, { serv: 'h', ris: 'h'.repeat(10) + 'g'.repeat(8), vinc: null }, ] await wrapper.vm.$nextTick() expect(wrapper.vm.punt).toEqual({ home: 10, guest: 8 }) expect(wrapper.vm.set).toEqual({ home: 1, guest: 0 }) // ultimo punto 'g' → serve guest expect(wrapper.vm.servHome).toBe(false) }) }) describe('connessione', () => { it('apre una WebSocket verso /ws al mount', () => { mountWith() const ws = ultimaWs() expect(ws).toBeDefined() expect(ws.url).toMatch(/^ws:\/\/.+\/ws$/) }) it('invia il messaggio register all\'apertura', () => { const wrapper = mountWith('controller') const ws = ultimaWs() ws.readyState = MockWebSocket.OPEN ws.onopen() expect(ws.send).toHaveBeenCalledTimes(1) const msg = JSON.parse(ws.send.mock.calls[0][0]) expect(msg).toEqual({ type: 'register', role: 'controller' }) expect(wrapper.vm.wsConnected).toBe(true) }) it('un messaggio "state" aggiorna lo stato locale', () => { const wrapper = mountWith() const ws = ultimaWs() const nuovo = createInitialState() nuovo.sp.nomi.home = 'Nuova Squadra' ws.onmessage({ data: JSON.stringify({ type: 'state', state: nuovo }) }) expect(wrapper.vm.state.sp.nomi.home).toBe('Nuova Squadra') }) it('un messaggio non-state invoca l\'hook onWsMessage', () => { const onWsMessage = vi.fn() const wrapper = mountWith('display', { methods: { onWsMessage } }) const ws = ultimaWs() ws.onmessage({ data: JSON.stringify({ type: 'speak', text: 'ciao' }) }) expect(onWsMessage).toHaveBeenCalledWith({ type: 'speak', text: 'ciao' }) }) }) describe('riconnessione', () => { it('scheduleReconnect usa backoff esponenziale con cap a 30s', () => { const wrapper = mountWith() const delays = [] const spy = vi.spyOn(globalThis, 'setTimeout').mockImplementation(() => 123) // intercetta i delay leggendoli dalle chiamate spy.mockImplementation((_fn, d) => { delays.push(d); return 123 }) wrapper.vm.reconnectAttempts = 0 wrapper.vm.reconnectTimeout = null wrapper.vm.scheduleReconnect() // 1000 wrapper.vm.reconnectTimeout = null wrapper.vm.scheduleReconnect() // 2000 wrapper.vm.reconnectTimeout = null wrapper.vm.scheduleReconnect() // 4000 expect(delays).toEqual([1000, 2000, 4000]) // attempts alto → cap a 30000 delays.length = 0 wrapper.vm.reconnectAttempts = 20 wrapper.vm.reconnectTimeout = null wrapper.vm.scheduleReconnect() expect(delays[0]).toBe(30000) spy.mockRestore() }) it('non riconnette su chiusura pulita (1000/1001)', () => { const wrapper = mountWith() const ws = ultimaWs() const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect') ws.onclose({ code: 1000 }) ws.onclose({ code: 1001 }) expect(spy).not.toHaveBeenCalled() expect(wrapper.vm.wsConnected).toBe(false) }) it('riconnette su chiusura anomala (es. 1006)', () => { const wrapper = mountWith() const ws = ultimaWs() const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect') ws.onclose({ code: 1006 }) expect(spy).toHaveBeenCalled() }) }) describe('sendWs', () => { it('ritorna false se non connesso', () => { const wrapper = mountWith() wrapper.vm.wsConnected = false expect(wrapper.vm.sendWs({ type: 'action' })).toBe(false) }) it('serializza e invia se connesso e aperto', () => { const wrapper = mountWith() const ws = ultimaWs() ws.readyState = MockWebSocket.OPEN wrapper.vm.wsConnected = true const ok = wrapper.vm.sendWs({ type: 'action', action: { type: 'incPunt' } }) expect(ok).toBe(true) const inviato = JSON.parse(ws.send.mock.calls.at(-1)[0]) expect(inviato.type).toBe('action') }) }) describe('cleanup', () => { it('beforeUnmount chiude la WebSocket', () => { const wrapper = mountWith() const ws = ultimaWs() wrapper.unmount() expect(ws.close).toHaveBeenCalled() }) }) })