boardgame.io
Version:
library for turn-based games
329 lines (287 loc) • 8.78 kB
text/typescript
/*
* Copyright 2017 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
import { SocketIOTransport } from './socketio';
import type * as ioNamespace from 'socket.io-client';
import { makeMove } from '../../core/action-creators';
import type { Master } from '../../master/master';
import type { ChatMessage, State } from '../../types';
import { ProcessGameConfig } from '../../core/game';
jest.mock('../../core/logger', () => ({
info: jest.fn(),
error: jest.fn(),
}));
type UpdateArgs = Parameters<Master['onUpdate']>;
type SyncArgs = Parameters<Master['onSync']>;
class MockSocket {
callbacks: Record<string, (arg0?: any, arg1?: any) => void>;
emit: jest.Mock;
constructor() {
this.callbacks = {};
this.emit = jest.fn();
}
receive(type: string, ...args) {
this.callbacks[type](...args);
}
on(type: string, callback: (arg0?: any, arg1?: any) => void) {
this.callbacks[type] = callback;
}
close() {}
}
test('defaults', () => {
const m = new SocketIOTransport({
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
expect(typeof (m as any).connectionStatusCallback).toBe('function');
(m as any).connectionStatusCallback();
});
class TransportAdapter extends SocketIOTransport {
declare socket: ioNamespace.Socket & {
io: { engine: any };
};
getMatchID() {
return this.matchID;
}
getPlayerID() {
return this.playerID;
}
getCredentials() {
return this.credentials;
}
}
describe('update matchID / playerID / credentials', () => {
const socket = new MockSocket();
const m = new TransportAdapter({
socket,
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
beforeEach(() => (socket.emit = jest.fn()));
test('matchID', () => {
m.updateMatchID('test');
expect(m.getMatchID()).toBe('test');
const args: SyncArgs = ['test', null, undefined, 2];
expect(socket.emit).lastCalledWith('sync', ...args);
});
test('playerID', () => {
m.updatePlayerID('player');
expect(m.getPlayerID()).toBe('player');
const args: SyncArgs = ['test', 'player', undefined, 2];
expect(socket.emit).lastCalledWith('sync', ...args);
});
test('credentials', () => {
m.updateCredentials('1234');
expect(m.getCredentials()).toBe('1234');
const args: SyncArgs = ['test', 'player', '1234', 2];
expect(socket.emit).lastCalledWith('sync', ...args);
});
});
describe('connection status', () => {
let onChangeMock: jest.Mock;
let mockSocket: MockSocket;
let m: SocketIOTransport;
beforeEach(() => {
onChangeMock = jest.fn();
mockSocket = new MockSocket();
m = new SocketIOTransport({
socket: mockSocket,
matchID: '0',
playerID: '0',
gameName: 'foo',
game: ProcessGameConfig({}),
gameKey: {},
numPlayers: 2,
transportDataCallback: () => {},
});
m.subscribeToConnectionStatus(onChangeMock);
m.connect();
});
test('connect', () => {
mockSocket.callbacks['connect']();
expect(onChangeMock).toHaveBeenCalled();
expect(m.isConnected).toBe(true);
});
test('disconnect', () => {
mockSocket.callbacks['disconnect']();
expect(onChangeMock).toHaveBeenCalled();
expect(m.isConnected).toBe(false);
});
test('close socket', () => {
mockSocket.callbacks['connect']();
expect(m.isConnected).toBe(true);
m.disconnect();
expect(m.isConnected).toBe(false);
});
test('doesn’t crash if syncing before connecting', () => {
const transportDataCallback = jest.fn();
const transport = new SocketIOTransport({
transportDataCallback,
game: ProcessGameConfig({}),
gameKey: {},
});
transport.requestSync();
expect(transportDataCallback).not.toHaveBeenCalled();
});
});
describe('multiplayer', () => {
const mockSocket = new MockSocket();
const transportDataCallback = jest.fn();
const transport = new TransportAdapter({
socket: mockSocket,
transportDataCallback,
game: ProcessGameConfig({}),
gameKey: {},
});
transport.connect();
beforeEach(jest.clearAllMocks);
test('receive update', () => {
const restored: { restore: boolean; _stateID?: number } = { restore: true };
mockSocket.receive('update', 'default', restored);
expect(transportDataCallback).toHaveBeenCalledWith({
type: 'update',
args: ['default', restored, undefined],
});
});
test('receive sync', () => {
const restored = { restore: true };
mockSocket.receive('sync', 'default', { state: restored });
expect(transportDataCallback).toHaveBeenCalledWith({
type: 'sync',
args: ['default', { state: restored }],
});
});
test('receive matchData', () => {
const matchData = [{ id: '0', name: 'Alice' }];
mockSocket.receive('matchData', 'default', matchData);
expect(transportDataCallback).toHaveBeenCalledWith({
type: 'matchData',
args: ['default', matchData],
});
});
test('send update', () => {
const action = makeMove(undefined, undefined, undefined);
const state = { _stateID: 0 } as State;
transport.sendAction(state, action);
const args: UpdateArgs = [action, state._stateID, 'default', null];
expect(mockSocket.emit).lastCalledWith('update', ...args);
});
test('receive chat-message', () => {
const chatData = { message: 'foo' };
mockSocket.receive('chat', 'default', chatData);
expect(transportDataCallback).toHaveBeenCalledWith({
type: 'chat',
args: ['default', chatData],
});
});
test('send chat-message', () => {
const message: ChatMessage = {
id: '0',
sender: '0',
payload: { message: 'foo' },
};
transport.sendChatMessage('matchID', message);
expect(mockSocket.emit).lastCalledWith(
'chat',
'matchID',
message,
transport.getCredentials()
);
});
});
describe('multiplayer delta state', () => {
const mockSocket = new MockSocket();
const transportDataCallback = jest.fn();
const transport = new TransportAdapter({
socket: mockSocket,
transportDataCallback,
game: ProcessGameConfig({}),
gameKey: {},
});
transport.connect();
beforeEach(jest.clearAllMocks);
test('receive patch', () => {
const patch1 = [
'default',
0,
1,
[{ op: 'replace', path: '/_stateID', value: 1 }],
[],
];
mockSocket.receive('patch', ...patch1);
expect(transportDataCallback).toHaveBeenCalledWith({
type: 'patch',
args: patch1,
});
});
});
describe('server option', () => {
const hostname = 'host';
const port = '1234';
test('without protocol', () => {
const server = hostname + ':' + port;
const m = new TransportAdapter({
server,
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
m.connect();
expect(m.socket.io.engine.hostname).toEqual(hostname);
expect(m.socket.io.engine.port).toEqual(port);
expect(m.socket.io.engine.secure).toEqual(false);
});
test('without trailing slash', () => {
const server = 'http://' + hostname + ':' + port;
const m = new SocketIOTransport({
server,
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
m.connect();
expect((m.socket.io as any).uri).toEqual(server + '/default');
});
test('https', () => {
const serverWithProtocol = 'https://' + hostname + ':' + port + '/';
const m = new TransportAdapter({
server: serverWithProtocol,
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
m.connect();
expect(m.socket.io.engine.hostname).toEqual(hostname);
expect(m.socket.io.engine.port).toEqual(port);
expect(m.socket.io.engine.secure).toEqual(true);
});
test('http', () => {
const serverWithProtocol = 'http://' + hostname + ':' + port + '/';
const m = new TransportAdapter({
server: serverWithProtocol,
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
m.connect();
expect(m.socket.io.engine.hostname).toEqual(hostname);
expect(m.socket.io.engine.port).toEqual(port);
expect(m.socket.io.engine.secure).toEqual(false);
});
test('no server set', () => {
const m = new TransportAdapter({
transportDataCallback: () => {},
game: ProcessGameConfig({}),
gameKey: {},
});
m.connect();
expect(m.socket.io.engine.hostname).not.toEqual(hostname);
expect(m.socket.io.engine.port).not.toEqual(port);
});
});