cbf927255a
Aggiunge test per beforeUnmount con connessione OPEN e con ws.close che lancia, l'annullamento del reconnectTimeout allo smontaggio, la guardia isConnecting, la chiusura della WebSocket precedente prima di riconnettersi (incluso il caso in cui close lancia), gli errori di registrazione/parsing nei gestori onopen/onmessage e la guardia di scheduleReconnect contro timer doppi.
326 lines
13 KiB
JavaScript
326 lines
13 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()
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|