implement test coverage for game layer

This commit is contained in:
seavor 2026-04-12 11:33:55 -05:00
parent 74803442d2
commit 367852866f
12 changed files with 2721 additions and 24 deletions

View file

@ -0,0 +1,123 @@
import { ArrowInfo, CardInfo, CounterInfo, PlayerProperties } from 'types';
import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces';
export function makeCard(overrides: Partial<CardInfo> = {}): CardInfo {
return {
id: 1,
name: 'Test Card',
x: 0,
y: 0,
faceDown: false,
tapped: false,
attacking: false,
color: '',
pt: '',
annotation: '',
destroyOnZoneChange: false,
doesntUntap: false,
counterList: [],
attachPlayerId: -1,
attachZone: '',
attachCardId: -1,
providerId: '',
...overrides,
};
}
export function makeCounter(overrides: Partial<CounterInfo> = {}): CounterInfo {
return {
id: 1,
name: 'Life',
counterColor: { r: 0, g: 0, b: 0, a: 255 },
radius: 1,
count: 20,
...overrides,
};
}
export function makeArrow(overrides: Partial<ArrowInfo> = {}): ArrowInfo {
return {
id: 1,
startPlayerId: 1,
startZone: 'table',
startCardId: 1,
targetPlayerId: 1,
targetZone: 'table',
targetCardId: 2,
arrowColor: { r: 255, g: 0, b: 0, a: 255 },
...overrides,
};
}
export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
return {
name: 'hand',
type: 1,
withCoords: false,
cardCount: 0,
cards: [],
alwaysRevealTopCard: false,
alwaysLookAtTopCard: false,
...overrides,
};
}
export function makePlayerProperties(overrides: Partial<PlayerProperties> = {}): PlayerProperties {
return {
playerId: 1,
userInfo: null,
spectator: false,
conceded: false,
readyStart: false,
deckHash: '',
pingSeconds: 0,
sideboardLocked: false,
judge: false,
...overrides,
};
}
export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEntry {
return {
properties: makePlayerProperties(),
deckList: '',
zones: {
hand: makeZoneEntry({ name: 'hand' }),
deck: makeZoneEntry({ name: 'deck' }),
},
counters: {},
arrows: {},
...overrides,
};
}
export function makeGameEntry(overrides: Partial<GameEntry> = {}): GameEntry {
return {
gameId: 1,
roomId: 1,
description: 'Test Game',
hostId: 1,
localPlayerId: 1,
spectator: false,
judge: false,
started: false,
activePlayerId: 0,
activePhase: 0,
secondsElapsed: 0,
reversed: false,
players: {
1: makePlayerEntry(),
},
messages: [],
...overrides,
};
}
export function makeState(overrides: Partial<GamesState> = {}): GamesState {
return {
games: {
1: makeGameEntry(),
},
...overrides,
};
}

View file

@ -0,0 +1,175 @@
import { Actions } from './game.actions';
import { Types } from './game.types';
import {
makeArrow,
makeCard,
makeCounter,
makeGameEntry,
makePlayerProperties,
makeZoneEntry,
} from './__mocks__/fixtures';
describe('Actions', () => {
it('clearStore', () => {
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
});
it('gameJoined', () => {
const entry = makeGameEntry();
expect(Actions.gameJoined(1, entry)).toEqual({ type: Types.GAME_JOINED, gameId: 1, gameEntry: entry });
});
it('gameLeft', () => {
expect(Actions.gameLeft(2)).toEqual({ type: Types.GAME_LEFT, gameId: 2 });
});
it('gameClosed', () => {
expect(Actions.gameClosed(3)).toEqual({ type: Types.GAME_CLOSED, gameId: 3 });
});
it('gameHostChanged', () => {
expect(Actions.gameHostChanged(1, 7)).toEqual({ type: Types.GAME_HOST_CHANGED, gameId: 1, hostId: 7 });
});
it('gameStateChanged', () => {
const data = { playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0 };
expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data });
});
it('playerJoined', () => {
const props = makePlayerProperties();
expect(Actions.playerJoined(1, props)).toEqual({ type: Types.PLAYER_JOINED, gameId: 1, playerProperties: props });
});
it('playerLeft', () => {
expect(Actions.playerLeft(1, 2, 3)).toEqual({ type: Types.PLAYER_LEFT, gameId: 1, playerId: 2, reason: 3 });
});
it('playerPropertiesChanged', () => {
const props = makePlayerProperties();
expect(Actions.playerPropertiesChanged(1, 2, props)).toEqual({
type: Types.PLAYER_PROPERTIES_CHANGED,
gameId: 1,
playerId: 2,
properties: props,
});
});
it('kicked', () => {
expect(Actions.kicked(1)).toEqual({ type: Types.KICKED, gameId: 1 });
});
it('cardMoved', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data });
});
it('cardFlipped', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data });
});
it('cardDestroyed', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data });
});
it('cardAttached', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data });
});
it('tokenCreated', () => {
const data = { cardId: 1 } as any;
expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data });
});
it('cardAttrChanged', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data });
});
it('cardCounterChanged', () => {
const data = { cardId: 1 } as any;
expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data });
});
it('arrowCreated', () => {
const arrow = makeArrow();
const data = { arrowInfo: arrow };
expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data });
});
it('arrowDeleted', () => {
const data = { arrowId: 3 };
expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data });
});
it('counterCreated', () => {
const counter = makeCounter();
const data = { counterInfo: counter };
expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data });
});
it('counterSet', () => {
const data = { counterId: 1, value: 10 };
expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data });
});
it('counterDeleted', () => {
const data = { counterId: 1 };
expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data });
});
it('cardsDrawn', () => {
const card = makeCard();
const data = { number: 2, cards: [card] };
expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data });
});
it('cardsRevealed', () => {
const data = { zoneName: 'hand', cards: [] } as any;
expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data });
});
it('zoneShuffled', () => {
const data = { zoneName: 'deck', start: 0, end: 39 };
expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data });
});
it('dieRolled', () => {
const data = { sides: 6, value: 4, values: [4] };
expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data });
});
it('activePlayerSet', () => {
expect(Actions.activePlayerSet(1, 3)).toEqual({ type: Types.ACTIVE_PLAYER_SET, gameId: 1, activePlayerId: 3 });
});
it('activePhaseSet', () => {
expect(Actions.activePhaseSet(1, 2)).toEqual({ type: Types.ACTIVE_PHASE_SET, gameId: 1, phase: 2 });
});
it('turnReversed', () => {
expect(Actions.turnReversed(1, true)).toEqual({ type: Types.TURN_REVERSED, gameId: 1, reversed: true });
});
it('zoneDumped', () => {
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data });
});
it('zonePropertiesChanged', () => {
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({
type: Types.ZONE_PROPERTIES_CHANGED,
gameId: 1,
playerId: 2,
data,
});
});
it('gameSay', () => {
expect(Actions.gameSay(1, 2, 'hello')).toEqual({ type: Types.GAME_SAY, gameId: 1, playerId: 2, message: 'hello' });
});
});

View file

@ -0,0 +1,198 @@
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } }));
import { store } from 'store/store';
import { Actions } from './game.actions';
import { Dispatch } from './game.dispatch';
import {
makeArrow,
makeCard,
makeCounter,
makeGameEntry,
makePlayerProperties,
} from './__mocks__/fixtures';
beforeEach(() => jest.clearAllMocks());
describe('Dispatch', () => {
it('clearStore dispatches Actions.clearStore()', () => {
Dispatch.clearStore();
expect(store.dispatch).toHaveBeenCalledWith(Actions.clearStore());
});
it('gameJoined dispatches Actions.gameJoined()', () => {
const entry = makeGameEntry();
Dispatch.gameJoined(1, entry);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameJoined(1, entry));
});
it('gameLeft dispatches Actions.gameLeft()', () => {
Dispatch.gameLeft(2);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameLeft(2));
});
it('gameClosed dispatches Actions.gameClosed()', () => {
Dispatch.gameClosed(3);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameClosed(3));
});
it('gameHostChanged dispatches Actions.gameHostChanged()', () => {
Dispatch.gameHostChanged(1, 7);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameHostChanged(1, 7));
});
it('gameStateChanged dispatches Actions.gameStateChanged()', () => {
const data = { playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0 };
Dispatch.gameStateChanged(1, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameStateChanged(1, data));
});
it('playerJoined dispatches Actions.playerJoined()', () => {
const props = makePlayerProperties();
Dispatch.playerJoined(1, props);
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerJoined(1, props));
});
it('playerLeft dispatches Actions.playerLeft()', () => {
Dispatch.playerLeft(1, 2, 3);
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerLeft(1, 2, 3));
});
it('playerPropertiesChanged dispatches Actions.playerPropertiesChanged()', () => {
const props = makePlayerProperties();
Dispatch.playerPropertiesChanged(1, 2, props);
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerPropertiesChanged(1, 2, props));
});
it('kicked dispatches Actions.kicked()', () => {
Dispatch.kicked(1);
expect(store.dispatch).toHaveBeenCalledWith(Actions.kicked(1));
});
it('cardMoved dispatches Actions.cardMoved()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardMoved(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data));
});
it('cardFlipped dispatches Actions.cardFlipped()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardFlipped(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data));
});
it('cardDestroyed dispatches Actions.cardDestroyed()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardDestroyed(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data));
});
it('cardAttached dispatches Actions.cardAttached()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardAttached(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data));
});
it('tokenCreated dispatches Actions.tokenCreated()', () => {
const data = { cardId: 1 } as any;
Dispatch.tokenCreated(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data));
});
it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardAttrChanged(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data));
});
it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => {
const data = { cardId: 1 } as any;
Dispatch.cardCounterChanged(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data));
});
it('arrowCreated dispatches Actions.arrowCreated()', () => {
const data = { arrowInfo: makeArrow() };
Dispatch.arrowCreated(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data));
});
it('arrowDeleted dispatches Actions.arrowDeleted()', () => {
const data = { arrowId: 3 };
Dispatch.arrowDeleted(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data));
});
it('counterCreated dispatches Actions.counterCreated()', () => {
const data = { counterInfo: makeCounter() };
Dispatch.counterCreated(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data));
});
it('counterSet dispatches Actions.counterSet()', () => {
const data = { counterId: 1, value: 10 };
Dispatch.counterSet(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data));
});
it('counterDeleted dispatches Actions.counterDeleted()', () => {
const data = { counterId: 1 };
Dispatch.counterDeleted(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data));
});
it('cardsDrawn dispatches Actions.cardsDrawn()', () => {
const data = { number: 2, cards: [makeCard()] };
Dispatch.cardsDrawn(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data));
});
it('cardsRevealed dispatches Actions.cardsRevealed()', () => {
const data = { zoneName: 'hand', cards: [] } as any;
Dispatch.cardsRevealed(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data));
});
it('zoneShuffled dispatches Actions.zoneShuffled()', () => {
const data = { zoneName: 'deck', start: 0, end: 39 };
Dispatch.zoneShuffled(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data));
});
it('dieRolled dispatches Actions.dieRolled()', () => {
const data = { sides: 6, value: 4, values: [4] };
Dispatch.dieRolled(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data));
});
it('activePlayerSet dispatches Actions.activePlayerSet()', () => {
Dispatch.activePlayerSet(1, 3);
expect(store.dispatch).toHaveBeenCalledWith(Actions.activePlayerSet(1, 3));
});
it('activePhaseSet dispatches Actions.activePhaseSet()', () => {
Dispatch.activePhaseSet(1, 2);
expect(store.dispatch).toHaveBeenCalledWith(Actions.activePhaseSet(1, 2));
});
it('turnReversed dispatches Actions.turnReversed()', () => {
Dispatch.turnReversed(1, true);
expect(store.dispatch).toHaveBeenCalledWith(Actions.turnReversed(1, true));
});
it('zoneDumped dispatches Actions.zoneDumped()', () => {
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
Dispatch.zoneDumped(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data));
});
it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => {
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
Dispatch.zonePropertiesChanged(1, 2, data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data));
});
it('gameSay dispatches Actions.gameSay()', () => {
Dispatch.gameSay(1, 2, 'gg wp');
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameSay(1, 2, 'gg wp'));
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,158 @@
import { Selectors } from './game.selectors';
import {
makeGameEntry, makePlayerEntry, makePlayerProperties, makeState,
makeZoneEntry, makeCard, makeCounter, makeArrow,
} from './__mocks__/fixtures';
import { GamesState } from './game.interfaces';
function rootState(games: GamesState) {
return { games };
}
describe('Selectors', () => {
it('getGames → returns the games map', () => {
const state = makeState();
expect(Selectors.getGames(rootState(state))).toBe(state.games);
});
it('getGame → returns the game entry for a given gameId', () => {
const state = makeState();
expect(Selectors.getGame(rootState(state), 1)).toBe(state.games[1]);
});
it('getGame → returns undefined for unknown gameId', () => {
const state = makeState();
expect(Selectors.getGame(rootState(state), 999)).toBeUndefined();
});
it('getPlayers → returns players map for a game', () => {
const state = makeState();
expect(Selectors.getPlayers(rootState(state), 1)).toBe(state.games[1].players);
});
it('getPlayers → returns undefined for unknown gameId', () => {
const state = makeState();
expect(Selectors.getPlayers(rootState(state), 999)).toBeUndefined();
});
it('getPlayer → returns a specific player', () => {
const state = makeState();
expect(Selectors.getPlayer(rootState(state), 1, 1)).toBe(state.games[1].players[1]);
});
it('getLocalPlayerId → returns localPlayerId from game', () => {
const state = makeState({ games: { 1: makeGameEntry({ localPlayerId: 42 }) } });
expect(Selectors.getLocalPlayerId(rootState(state), 1)).toBe(42);
});
it('getLocalPlayer → returns the player matching localPlayerId', () => {
const state = makeState({ games: { 1: makeGameEntry({ localPlayerId: 1 }) } });
const result = Selectors.getLocalPlayer(rootState(state), 1);
expect(result).toBe(state.games[1].players[1]);
});
it('getLocalPlayer → returns undefined when game is not found', () => {
const state = makeState();
expect(Selectors.getLocalPlayer(rootState(state), 999)).toBeUndefined();
});
it('getZones → returns zones map for a player', () => {
const state = makeState();
expect(Selectors.getZones(rootState(state), 1, 1)).toBe(state.games[1].players[1].zones);
});
it('getZone → returns a specific zone', () => {
const state = makeState();
expect(Selectors.getZone(rootState(state), 1, 1, 'hand')).toBe(state.games[1].players[1].zones['hand']);
});
it('getCards → returns cards array for a zone', () => {
const card = makeCard();
const state = makeState({
games: {
1: makeGameEntry({
players: {
1: makePlayerEntry({
zones: { hand: makeZoneEntry({ name: 'hand', cards: [card] }) },
}),
},
}),
},
});
expect(Selectors.getCards(rootState(state), 1, 1, 'hand')).toEqual([card]);
});
it('getCards → returns [] when zone not found', () => {
const state = makeState();
expect(Selectors.getCards(rootState(state), 1, 1, 'nonexistent')).toEqual([]);
});
it('getCounters → returns counters map for a player', () => {
const counter = makeCounter({ id: 2 });
const state = makeState({
games: { 1: makeGameEntry({ players: { 1: makePlayerEntry({ counters: { 2: counter } }) } }) },
});
expect(Selectors.getCounters(rootState(state), 1, 1)).toEqual({ 2: counter });
});
it('getArrows → returns arrows map for a player', () => {
const arrow = makeArrow({ id: 3 });
const state = makeState({
games: { 1: makeGameEntry({ players: { 1: makePlayerEntry({ arrows: { 3: arrow } }) } }) },
});
expect(Selectors.getArrows(rootState(state), 1, 1)).toEqual({ 3: arrow });
});
it('getActivePlayerId → returns activePlayerId from game', () => {
const state = makeState({ games: { 1: makeGameEntry({ activePlayerId: 7 }) } });
expect(Selectors.getActivePlayerId(rootState(state), 1)).toBe(7);
});
it('getActivePhase → returns activePhase from game', () => {
const state = makeState({ games: { 1: makeGameEntry({ activePhase: 3 }) } });
expect(Selectors.getActivePhase(rootState(state), 1)).toBe(3);
});
it('isStarted → returns true when game is started', () => {
const state = makeState({ games: { 1: makeGameEntry({ started: true }) } });
expect(Selectors.isStarted(rootState(state), 1)).toBe(true);
});
it('isStarted → returns false when game not found', () => {
const state = makeState();
expect(Selectors.isStarted(rootState(state), 999)).toBe(false);
});
it('isSpectator → returns spectator flag from game', () => {
const state = makeState({ games: { 1: makeGameEntry({ spectator: true }) } });
expect(Selectors.isSpectator(rootState(state), 1)).toBe(true);
});
it('isReversed → returns reversed flag from game', () => {
const state = makeState({ games: { 1: makeGameEntry({ reversed: true }) } });
expect(Selectors.isReversed(rootState(state), 1)).toBe(true);
});
it('getMessages → returns messages array from game', () => {
const messages = [{ playerId: 1, message: 'hi', timeReceived: 100 }];
const state = makeState({ games: { 1: makeGameEntry({ messages }) } });
expect(Selectors.getMessages(rootState(state), 1)).toBe(messages);
});
it('getMessages → returns [] when game not found', () => {
const state = makeState();
expect(Selectors.getMessages(rootState(state), 999)).toEqual([]);
});
it('getActiveGameIds → returns numeric array of gameIds', () => {
const state = makeState({
games: {
1: makeGameEntry({ gameId: 1 }),
2: makeGameEntry({ gameId: 2 }),
},
});
const ids = Selectors.getActiveGameIds(rootState(state));
expect(ids).toEqual(expect.arrayContaining([1, 2]));
expect(ids).toHaveLength(2);
});
});