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 " → setNomi con entrambi i nomi', () => { sendLine('nomi Antoniana Riviera') expect(lastSent()).toMatchObject({ type: 'action', action: { type: 'setNomi', home: 'Antoniana', guest: 'Riviera' }, }) }) it('"nomi " → 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() }) })