99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { ioMock } = vi.hoisted(() => ({
|
|
ioMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('socket.io-client', () => ({
|
|
io: ioMock,
|
|
}));
|
|
|
|
import { destroySocket, getSocket } from './socket';
|
|
|
|
interface MockChatSocket {
|
|
on: ReturnType<typeof vi.fn>;
|
|
offAny: ReturnType<typeof vi.fn>;
|
|
disconnect: ReturnType<typeof vi.fn>;
|
|
/** Test-only helper: fires every handler registered for `event` via
|
|
* `.on`, mirroring how a real socket.io-client instance invokes its own
|
|
* listeners (e.g. calling the registered `disconnect` handler(s) on a
|
|
* real transient disconnect). */
|
|
trigger(event: string): void;
|
|
}
|
|
|
|
function createMockSocket(): MockChatSocket {
|
|
const handlers = new Map<string, Set<() => void>>();
|
|
const mockSocket: MockChatSocket = {
|
|
on: vi.fn((event: string, handler: () => void) => {
|
|
if (!handlers.has(event)) handlers.set(event, new Set());
|
|
handlers.get(event)?.add(handler);
|
|
return mockSocket;
|
|
}),
|
|
offAny: vi.fn(() => mockSocket),
|
|
disconnect: vi.fn(() => mockSocket),
|
|
trigger(event: string): void {
|
|
for (const handler of handlers.get(event) ?? []) handler();
|
|
},
|
|
};
|
|
return mockSocket;
|
|
}
|
|
|
|
let currentMock!: MockChatSocket;
|
|
|
|
describe('chat socket', () => {
|
|
beforeEach(() => {
|
|
ioMock.mockReset();
|
|
// A fresh object per io() call so identity assertions (same singleton vs.
|
|
// a genuinely new instance) are meaningful.
|
|
ioMock.mockImplementation(() => {
|
|
currentMock = createMockSocket();
|
|
return currentMock;
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
destroySocket();
|
|
});
|
|
|
|
it('creates one same-origin /chat namespace socket', () => {
|
|
const first = getSocket();
|
|
const second = getSocket();
|
|
|
|
expect(first).toBe(second);
|
|
expect(ioMock).toHaveBeenCalledOnce();
|
|
expect(ioMock).toHaveBeenCalledWith('/chat', {
|
|
withCredentials: true,
|
|
autoConnect: false,
|
|
transports: ['websocket', 'polling'],
|
|
});
|
|
});
|
|
|
|
it('keeps the same singleton instance across a transient disconnect', () => {
|
|
const first = getSocket();
|
|
|
|
// socket.ts must not react to a real socket's `disconnect` event by
|
|
// nulling the singleton — it registers no such handler at all now.
|
|
// Actually fire every handler registered via `.on('disconnect', ...)`
|
|
// (mirroring a real socket.io-client reconnect) instead of merely
|
|
// calling getSocket() again: this is what makes the test fail if
|
|
// production reintroduces `socket.on('disconnect', () => { socket =
|
|
// null; })`, since that handler would run here and null the singleton
|
|
// before the next getSocket() call.
|
|
currentMock.trigger('disconnect');
|
|
const second = getSocket();
|
|
|
|
expect(second).toBe(first);
|
|
expect(ioMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('only creates a new singleton after an explicit destroySocket()', () => {
|
|
const first = getSocket();
|
|
|
|
destroySocket();
|
|
const second = getSocket();
|
|
|
|
expect(second).not.toBe(first);
|
|
expect(ioMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|