2026-06-21 00:36:02 +02:00
|
|
|
// @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: '<div></div>',
|
|
|
|
|
...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()
|
|
|
|
|
})
|
2026-07-03 10:59:08 +02:00
|
|
|
|
|
|
|
|
it('allo scadere del timer riapre davvero la connessione', () => {
|
|
|
|
|
vi.useFakeTimers()
|
|
|
|
|
try {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
// il mock non fa mai scattare onopen: sblocca la guardia isConnecting
|
|
|
|
|
wrapper.vm.isConnecting = false
|
|
|
|
|
const prima = MockWebSocket.instances.length
|
|
|
|
|
wrapper.vm.scheduleReconnect()
|
|
|
|
|
vi.advanceTimersByTime(1000)
|
|
|
|
|
expect(MockWebSocket.instances.length).toBe(prima + 1)
|
|
|
|
|
expect(wrapper.vm.reconnectTimeout).toBe(null)
|
|
|
|
|
} finally {
|
|
|
|
|
vi.useRealTimers()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('se il costruttore WebSocket lancia, programma la riconnessione', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
wrapper.vm.isConnecting = false
|
|
|
|
|
const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect').mockImplementation(() => {})
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
vi.stubGlobal('WebSocket', class { constructor() { throw new Error('boom') } })
|
|
|
|
|
try {
|
|
|
|
|
wrapper.vm.connectWebSocket()
|
|
|
|
|
expect(spy).toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.vm.isConnecting).toBe(false)
|
|
|
|
|
expect(wrapper.vm.ws).toBe(null)
|
|
|
|
|
} finally {
|
|
|
|
|
vi.stubGlobal('WebSocket', MockWebSocket)
|
|
|
|
|
consoleSpy.mockRestore()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('onerror azzera lo stato di connessione', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
wrapper.vm.wsConnected = true
|
|
|
|
|
wrapper.vm.isConnecting = true
|
|
|
|
|
ws.onerror()
|
|
|
|
|
expect(wrapper.vm.wsConnected).toBe(false)
|
|
|
|
|
expect(wrapper.vm.isConnecting).toBe(false)
|
|
|
|
|
})
|
2026-06-21 00:36:02 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
})
|
2026-07-03 10:59:08 +02:00
|
|
|
|
|
|
|
|
it('ritorna false se send lancia', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
ws.readyState = MockWebSocket.OPEN
|
|
|
|
|
wrapper.vm.wsConnected = true
|
|
|
|
|
ws.send.mockImplementation(() => { throw new Error('boom') })
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
expect(wrapper.vm.sendWs({ type: 'action' })).toBe(false)
|
|
|
|
|
consoleSpy.mockRestore()
|
|
|
|
|
})
|
2026-06-21 00:36:02 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('cleanup', () => {
|
|
|
|
|
it('beforeUnmount chiude la WebSocket', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
expect(ws.close).toHaveBeenCalled()
|
|
|
|
|
})
|
2026-07-03 12:00:12 +02:00
|
|
|
|
|
|
|
|
it('beforeUnmount con connessione OPEN chiude con codice 1000', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
ws.readyState = MockWebSocket.OPEN
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
expect(ws.close).toHaveBeenCalledWith(1000, 'Component unmounting')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('beforeUnmount non lancia se ws.close lancia', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
ws.readyState = MockWebSocket.OPEN
|
|
|
|
|
ws.close.mockImplementation(() => { throw new Error('già chiusa') })
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
expect(() => wrapper.unmount()).not.toThrow()
|
|
|
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('beforeUnmount annulla un reconnectTimeout pendente', () => {
|
|
|
|
|
vi.useFakeTimers()
|
|
|
|
|
try {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
wrapper.vm.isConnecting = false
|
|
|
|
|
wrapper.vm.scheduleReconnect()
|
|
|
|
|
expect(wrapper.vm.reconnectTimeout).not.toBe(null)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
// se il timer non fosse stato annullato, allo scoccare del delay
|
|
|
|
|
// verrebbe creata una nuova connessione anche a componente smontato
|
|
|
|
|
const primaDelTimeout = MockWebSocket.instances.length
|
|
|
|
|
vi.advanceTimersByTime(30000)
|
|
|
|
|
expect(MockWebSocket.instances.length).toBe(primaDelTimeout)
|
|
|
|
|
} finally {
|
|
|
|
|
vi.useRealTimers()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('rami aggiuntivi', () => {
|
|
|
|
|
it('connectWebSocket non fa nulla se è già in corso un tentativo di connessione', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const primaDelTimeout = MockWebSocket.instances.length
|
|
|
|
|
wrapper.vm.isConnecting = true
|
|
|
|
|
wrapper.vm.connectWebSocket()
|
|
|
|
|
expect(MockWebSocket.instances.length).toBe(primaDelTimeout)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('riconnettendosi con la WebSocket precedente OPEN, la chiude con codice 1000', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const prima = ultimaWs()
|
|
|
|
|
prima.readyState = MockWebSocket.OPEN
|
|
|
|
|
wrapper.vm.isConnecting = false
|
|
|
|
|
wrapper.vm.connectWebSocket()
|
|
|
|
|
expect(prima.close).toHaveBeenCalledWith(1000, 'Reconnecting')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('se la chiusura della WebSocket precedente lancia, logga e continua a riconnettersi', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const prima = ultimaWs()
|
|
|
|
|
prima.readyState = MockWebSocket.OPEN
|
|
|
|
|
prima.close.mockImplementation(() => { throw new Error('già chiusa') })
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
wrapper.vm.isConnecting = false
|
|
|
|
|
|
|
|
|
|
expect(() => wrapper.vm.connectWebSocket()).not.toThrow()
|
|
|
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
|
|
|
expect(ultimaWs()).not.toBe(prima)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('onopen non lancia se il registro fallisce (send che lancia)', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
ws.readyState = MockWebSocket.OPEN
|
|
|
|
|
ws.send.mockImplementation(() => { throw new Error('boom') })
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
|
|
|
|
|
expect(() => ws.onopen()).not.toThrow()
|
|
|
|
|
expect(wrapper.vm.wsConnected).toBe(true)
|
|
|
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('onmessage non lancia su JSON non valido', () => {
|
|
|
|
|
mountWith()
|
|
|
|
|
const ws = ultimaWs()
|
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
|
|
|
|
|
expect(() => ws.onmessage({ data: 'non è JSON {{{' })).not.toThrow()
|
|
|
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('scheduleReconnect non arma un secondo timer se uno è già pendente', () => {
|
|
|
|
|
const wrapper = mountWith()
|
|
|
|
|
const spy = vi.spyOn(globalThis, 'setTimeout')
|
|
|
|
|
wrapper.vm.reconnectTimeout = 123 // timer già pendente (valore fittizio)
|
|
|
|
|
wrapper.vm.scheduleReconnect()
|
|
|
|
|
expect(spy).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-06-21 00:36:02 +02:00
|
|
|
})
|
|
|
|
|
})
|