32 test che coprono registrazione WebSocket, parsing di tutti i comandi (shortcut inclusi), validazioni input, conferma reset e gestione uscita.
304 lines
9.3 KiB
JavaScript
304 lines
9.3 KiB
JavaScript
import { vi, describe, it, expect, beforeAll, beforeEach } from 'vitest'
|
|
|
|
// vi.hoisted garantisce che refs sia disponibile nelle factory dei mock,
|
|
// che vengono hoistate prima degli import statici.
|
|
const refs = vi.hoisted(() => ({ ws: null, rl: null }))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock: ws
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('ws', async () => {
|
|
const { EventEmitter } = await import('events')
|
|
|
|
class WebSocket extends EventEmitter {
|
|
static OPEN = 1
|
|
constructor() {
|
|
super()
|
|
this.readyState = 1
|
|
this.send = vi.fn()
|
|
this.close = vi.fn()
|
|
refs.ws = this
|
|
}
|
|
}
|
|
|
|
return { WebSocket }
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock: readline
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('readline', async () => {
|
|
const { EventEmitter } = await import('events')
|
|
|
|
return {
|
|
default: {
|
|
createInterface: vi.fn(() => {
|
|
const rl = new EventEmitter()
|
|
rl.prompt = vi.fn()
|
|
rl.question = vi.fn()
|
|
rl.close = vi.fn()
|
|
refs.rl = rl
|
|
return rl
|
|
}),
|
|
},
|
|
}
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Silenzia output e blocca process.exit durante i test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
vi.spyOn(process.stdout, 'write').mockReturnValue(true)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Importa cli.js — esegue gli effetti collaterali con le dipendenze mockate
|
|
// ---------------------------------------------------------------------------
|
|
|
|
beforeAll(async () => {
|
|
await import('../../cli.js')
|
|
refs.ws.emit('open')
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function sendLine(line) {
|
|
refs.rl.emit('line', line)
|
|
}
|
|
|
|
function lastSent() {
|
|
const calls = refs.ws.send.mock.calls
|
|
return calls.length ? JSON.parse(calls[calls.length - 1][0]) : null
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('CLI — Registrazione', () => {
|
|
it('invia register con ruolo controller all\'apertura del WebSocket', () => {
|
|
const call = refs.ws.send.mock.calls.find((c) => {
|
|
try { return JSON.parse(c[0]).type === 'register' } catch { return false }
|
|
})
|
|
expect(call).toBeDefined()
|
|
expect(JSON.parse(call[0])).toEqual({ type: 'register', role: 'controller' })
|
|
})
|
|
})
|
|
|
|
describe('CLI — Ricezione messaggi', () => {
|
|
beforeEach(() => {
|
|
refs.rl.prompt.mockClear()
|
|
vi.mocked(console.error).mockClear()
|
|
})
|
|
|
|
it('ripristina il prompt dopo un messaggio "state"', () => {
|
|
const state = {
|
|
sp: { nomi: { home: 'A', guest: 'B' }, punt: { home: 0, guest: 0 }, set: { home: 0, guest: 0 }, servHome: true },
|
|
modalitaPartita: '3/5',
|
|
}
|
|
refs.ws.emit('message', JSON.stringify({ type: 'state', state }))
|
|
expect(refs.rl.prompt).toHaveBeenCalled()
|
|
})
|
|
|
|
it('mostra un errore alla ricezione di un messaggio "error"', () => {
|
|
refs.ws.emit('message', JSON.stringify({ type: 'error', message: 'azione non valida' }))
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CLI — Comandi punteggio', () => {
|
|
beforeEach(() => refs.ws.send.mockClear())
|
|
|
|
it('"+" → incPunt home', () => {
|
|
sendLine('+')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'home' } })
|
|
})
|
|
|
|
it('"pc" → incPunt home (shortcut)', () => {
|
|
sendLine('pc')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'home' } })
|
|
})
|
|
|
|
it('"-" → incPunt guest', () => {
|
|
sendLine('-')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'guest' } })
|
|
})
|
|
|
|
it('"po" → incPunt guest (shortcut)', () => {
|
|
sendLine('po')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'guest' } })
|
|
})
|
|
|
|
it('"punto casa" → incPunt home', () => {
|
|
sendLine('punto casa')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'home' } })
|
|
})
|
|
|
|
it('"punto ospite" → incPunt guest', () => {
|
|
sendLine('punto ospite')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incPunt', team: 'guest' } })
|
|
})
|
|
|
|
it('"punto" senza squadra → errore, nessun invio', () => {
|
|
sendLine('punto')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
|
|
it('"undo" → decPunt', () => {
|
|
sendLine('undo')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'decPunt' } })
|
|
})
|
|
|
|
it('"u" → decPunt (shortcut)', () => {
|
|
sendLine('u')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'decPunt' } })
|
|
})
|
|
})
|
|
|
|
describe('CLI — Comandi set', () => {
|
|
beforeEach(() => refs.ws.send.mockClear())
|
|
|
|
it('"set casa" → incSet home', () => {
|
|
sendLine('set casa')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incSet', team: 'home' } })
|
|
})
|
|
|
|
it('"set ospite" → incSet guest', () => {
|
|
sendLine('set ospite')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'incSet', team: 'guest' } })
|
|
})
|
|
|
|
it('"set" senza squadra → errore, nessun invio', () => {
|
|
sendLine('set')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CLI — Comandi partita', () => {
|
|
beforeEach(() => refs.ws.send.mockClear())
|
|
|
|
it('"serv" → cambiaPalla', () => {
|
|
sendLine('serv')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'cambiaPalla' } })
|
|
})
|
|
|
|
it('"nomi <casa> <ospite>" → setNomi con entrambi i nomi', () => {
|
|
sendLine('nomi Antoniana Riviera')
|
|
expect(lastSent()).toMatchObject({
|
|
type: 'action',
|
|
action: { type: 'setNomi', home: 'Antoniana', guest: 'Riviera' },
|
|
})
|
|
})
|
|
|
|
it('"nomi <casa>" → setNomi con solo nome casa', () => {
|
|
sendLine('nomi Antoniana')
|
|
const sent = lastSent()
|
|
expect(sent).toMatchObject({ type: 'action', action: { type: 'setNomi', home: 'Antoniana' } })
|
|
expect(sent.action.guest).toBeUndefined()
|
|
})
|
|
|
|
it('"nomi" senza argomenti → errore, nessun invio', () => {
|
|
sendLine('nomi')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
|
|
it('"modalita 2/3" → setModalita 2/3', () => {
|
|
sendLine('modalita 2/3')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'setModalita', modalita: '2/3' } })
|
|
})
|
|
|
|
it('"modalita 3/5" → setModalita 3/5', () => {
|
|
sendLine('modalita 3/5')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'setModalita', modalita: '3/5' } })
|
|
})
|
|
|
|
it('"modalita" con valore non valido → errore, nessun invio', () => {
|
|
sendLine('modalita 4/7')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CLI — Comando reset (con conferma)', () => {
|
|
beforeEach(() => {
|
|
refs.ws.send.mockClear()
|
|
refs.rl.question.mockClear()
|
|
})
|
|
|
|
it('conferma "s" → invia resetta', () => {
|
|
refs.rl.question.mockImplementationOnce((_msg, cb) => cb('s'))
|
|
sendLine('reset')
|
|
expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'resetta' } })
|
|
})
|
|
|
|
it('risposta "n" → non invia nulla', () => {
|
|
refs.rl.question.mockImplementationOnce((_msg, cb) => cb('n'))
|
|
sendLine('reset')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('risposta vuota → non invia nulla', () => {
|
|
refs.rl.question.mockImplementationOnce((_msg, cb) => cb(''))
|
|
sendLine('reset')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CLI — Comandi informativi', () => {
|
|
beforeEach(() => {
|
|
refs.ws.send.mockClear()
|
|
vi.mocked(console.log).mockClear()
|
|
})
|
|
|
|
it('"help" → stampa aiuto, nessun invio', () => {
|
|
sendLine('help')
|
|
expect(console.log).toHaveBeenCalled()
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('"stato" → nessun invio', () => {
|
|
sendLine('stato')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('comando sconosciuto → messaggio di errore, nessun invio', () => {
|
|
sendLine('xyzzy')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
expect(console.error).toHaveBeenCalled()
|
|
})
|
|
|
|
it('riga vuota → nessun invio', () => {
|
|
refs.rl.emit('line', ' ')
|
|
expect(refs.ws.send).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CLI — Uscita', () => {
|
|
it('"exit" → chiude il WebSocket', () => {
|
|
refs.ws.close.mockClear()
|
|
sendLine('exit')
|
|
expect(refs.ws.close).toHaveBeenCalled()
|
|
})
|
|
|
|
it('"q" → chiude il WebSocket (shortcut)', () => {
|
|
refs.ws.close.mockClear()
|
|
sendLine('q')
|
|
expect(refs.ws.close).toHaveBeenCalled()
|
|
})
|
|
|
|
it('chiusura readline → chiude il WebSocket', () => {
|
|
refs.ws.close.mockClear()
|
|
refs.rl.emit('close')
|
|
expect(refs.ws.close).toHaveBeenCalled()
|
|
})
|
|
})
|