b1c69f9517
Aggiunge i casi non ancora coperti: il setTimeout di scheduleReconnect riapre davvero la connessione, un costruttore WebSocket che lancia programma comunque il retry, onerror azzera lo stato, sendWs ritorna false se send lancia.
229 lines
8.5 KiB
JavaScript
229 lines
8.5 KiB
JavaScript
// @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()
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
|
|
describe('cleanup', () => {
|
|
it('beforeUnmount chiude la WebSocket', () => {
|
|
const wrapper = mountWith()
|
|
const ws = ultimaWs()
|
|
wrapper.unmount()
|
|
expect(ws.close).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|