Cockatrice/webclient/src/store/rooms/rooms.reducer.spec.ts
2026-04-14 11:34:29 -05:00

309 lines
14 KiB
TypeScript

import { GameSortField, SortDirection } from 'types';
import { roomsReducer } from './rooms.reducer';
import { Types, MAX_ROOM_MESSAGES } from './rooms.types';
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
// ── Initialisation ───────────────────────────────────────────────────────────
describe('Initialisation', () => {
it('returns initialState when called with undefined state', () => {
const result = roomsReducer(undefined, { type: '@@INIT' });
expect(result.rooms).toEqual({});
expect(result.joinedRoomIds).toEqual({});
});
it('CLEAR_STORE → resets to initialState', () => {
const state = makeRoomsState({ joinedRoomIds: { 1: true } });
const result = roomsReducer(state, { type: Types.CLEAR_STORE });
expect(result.joinedRoomIds).toEqual({});
expect(result.rooms).toEqual({});
});
it('default → returns state unchanged for unknown action', () => {
const state = makeRoomsState();
const result = roomsReducer(state, { type: '@@UNKNOWN' });
expect(result).toBe(state);
});
});
// ── UPDATE_ROOMS ──────────────────────────────────────────────────────────────
describe('UPDATE_ROOMS', () => {
it('merges rooms and strips gameList, gametypeList, userList from update', () => {
const state = makeRoomsState({ rooms: {} });
const room = { ...makeRoom({ roomId: 1 }), gameList: [makeGame()], userList: [makeUser()], gametypeList: ['standard'] };
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [room] });
expect(result.rooms[1]).toBeDefined();
expect(result.rooms[1].gameList).toBeUndefined();
expect(result.rooms[1].userList).toBeUndefined();
expect(result.rooms[1].gametypeList).toBeUndefined();
});
it('sets numeric order from array index', () => {
const state = makeRoomsState({ rooms: {} });
const rooms = [makeRoom({ roomId: 1 }), makeRoom({ roomId: 2 })];
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms });
expect(result.rooms[1].order).toBe(0);
expect(result.rooms[2].order).toBe(1);
});
it('merges into existing room entry (preserves existing fields)', () => {
const existingRoom = makeRoom({ roomId: 1, name: 'Old Name', gameList: [makeGame()] });
const state = makeRoomsState({ rooms: { 1: existingRoom } });
const update = makeRoom({ roomId: 1, name: 'New Name' });
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [update] });
expect(result.rooms[1].name).toBe('New Name');
expect(result.rooms[1].gameList).toEqual([makeGame()]);
});
it('creates new room entry for unknown roomId', () => {
const state = makeRoomsState({ rooms: {} });
const room = makeRoom({ roomId: 99, name: 'New Room' });
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [room] });
expect(result.rooms[99]).toBeDefined();
expect(result.rooms[99].name).toBe('New Room');
});
});
// ── JOIN_ROOM ──────────────────────────────────────────────────────────────────
describe('JOIN_ROOM', () => {
it('copies gameList and userList, sorts both, sets joinedRoomIds', () => {
const state = makeRoomsState({ rooms: {}, joinedRoomIds: {} });
const roomInfo = makeRoom({
roomId: 2,
gameList: [makeGame({ gameId: 1 })],
userList: [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })],
});
const result = roomsReducer(state, { type: Types.JOIN_ROOM, roomInfo });
expect(result.joinedRoomIds[2]).toBe(true);
expect(result.rooms[2].userList[0].name).toBe('Alice');
expect(result.rooms[2]).toMatchObject({ roomId: 2 });
});
});
// ── LEAVE_ROOM ────────────────────────────────────────────────────────────────
describe('LEAVE_ROOM', () => {
it('removes joinedRoomIds entry and messages for roomId', () => {
const state = makeRoomsState({
joinedRoomIds: { 1: true },
messages: { 1: [makeMessage()] },
});
const result = roomsReducer(state, { type: Types.LEAVE_ROOM, roomId: 1 });
expect(result.joinedRoomIds[1]).toBeUndefined();
expect(result.messages[1]).toBeUndefined();
});
});
// ── ADD_MESSAGE ───────────────────────────────────────────────────────────────
describe('ADD_MESSAGE', () => {
it('appends message with timeReceived set', () => {
const state = makeRoomsState({ messages: { 1: [] } });
const message = makeMessage({ message: 'hello', timeReceived: 0 });
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
expect(result.messages[1]).toHaveLength(1);
expect(result.messages[1][0].timeReceived).toBeGreaterThan(0);
});
it('creates message list for roomId when none exists', () => {
const state = makeRoomsState({ messages: {} });
const message = makeMessage();
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 5, message });
expect(result.messages[5]).toHaveLength(1);
});
it(`shifts oldest message when list is at MAX_ROOM_MESSAGES (${MAX_ROOM_MESSAGES})`, () => {
const firstMsg = makeMessage({ message: 'first' });
const messages = Array.from({ length: MAX_ROOM_MESSAGES }, (_, i) =>
i === 0 ? firstMsg : makeMessage({ message: `msg-${i}` })
);
const state = makeRoomsState({ messages: { 1: messages } });
const newMsg = makeMessage({ message: 'new' });
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message: newMsg });
expect(result.messages[1]).toHaveLength(MAX_ROOM_MESSAGES);
expect(result.messages[1][0].message).not.toBe('first');
expect(result.messages[1][MAX_ROOM_MESSAGES - 1].message).toBe('new');
});
it('prepends "name: " to message when name is present', () => {
const state = makeRoomsState({ messages: { 1: [] } });
const message = makeMessage({ name: 'Alice', message: 'hello' });
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
expect(result.messages[1][0].message).toBe('Alice: hello');
});
it('does not prepend when name is empty', () => {
const state = makeRoomsState({ messages: { 1: [] } });
const message = makeMessage({ name: '', message: 'system msg' });
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
expect(result.messages[1][0].message).toBe('system msg');
});
});
// ── UPDATE_GAMES ──────────────────────────────────────────────────────────────
describe('UPDATE_GAMES', () => {
it('removes closed games from gameList', () => {
const room = makeRoom({ roomId: 1, gameList: [makeGame({ gameId: 1 })] });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, {
type: Types.UPDATE_GAMES,
roomId: 1,
games: [{ gameId: 1, closed: true }],
});
expect(result.rooms[1].gameList).toHaveLength(0);
});
it('merges update into existing game', () => {
const game = makeGame({ gameId: 1, description: 'old' });
const room = makeRoom({ roomId: 1, gameList: [game] });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, {
type: Types.UPDATE_GAMES,
roomId: 1,
games: [{ gameId: 1, description: 'new' }],
});
expect(result.rooms[1].gameList[0].description).toBe('new');
});
it('appends new game to list and sorts', () => {
const room = makeRoom({ roomId: 1, gameList: [] });
const state = makeRoomsState({ rooms: { 1: room } });
const newGame = makeGame({ gameId: 99, description: 'extra' });
const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 1, games: [newGame] });
expect(result.rooms[1].gameList).toHaveLength(1);
expect(result.rooms[1].gameList[0].gameId).toBe(99);
});
it('preserves existing games not included in the update', () => {
const game1 = makeGame({ gameId: 1, description: 'untouched' });
const game2 = makeGame({ gameId: 2, description: 'old' });
const room = makeRoom({ roomId: 1, gameList: [game1, game2] });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, {
type: Types.UPDATE_GAMES,
roomId: 1,
games: [{ gameId: 2, description: 'new' }],
});
expect(result.rooms[1].gameList.find(g => g.gameId === 1).description).toBe('untouched');
expect(result.rooms[1].gameList.find(g => g.gameId === 2).description).toBe('new');
});
it('returns { ...state } (not identity) when roomId is unknown', () => {
const state = makeRoomsState({ rooms: {} });
const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 999, games: [] });
expect(result).not.toBe(state);
expect(result.rooms).toEqual(state.rooms);
});
});
// ── USER_JOINED / USER_LEFT ───────────────────────────────────────────────────
describe('USER_JOINED', () => {
it('appends user to userList and sorts by name ASC', () => {
const room = makeRoom({ roomId: 1, userList: [makeUser({ name: 'Zane' })] });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, { type: Types.USER_JOINED, roomId: 1, user: makeUser({ name: 'Alice' }) });
expect(result.rooms[1].userList[0].name).toBe('Alice');
expect(result.rooms[1].userList).toHaveLength(2);
});
});
describe('USER_LEFT', () => {
it('removes user by name from userList', () => {
const room = makeRoom({ roomId: 1, userList: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, { type: Types.USER_LEFT, roomId: 1, name: 'Alice' });
expect(result.rooms[1].userList).toHaveLength(1);
expect(result.rooms[1].userList[0].name).toBe('Bob');
});
});
// ── SORT_GAMES ────────────────────────────────────────────────────────────────
describe('SORT_GAMES', () => {
it('resorts gameList and updates sortGamesBy on state', () => {
const games = [makeGame({ gameId: 2 }), makeGame({ gameId: 1 })];
const room = makeRoom({ roomId: 1, gameList: games });
const state = makeRoomsState({ rooms: { 1: room } });
const result = roomsReducer(state, {
type: Types.SORT_GAMES,
roomId: 1,
field: GameSortField.START_TIME,
order: SortDirection.ASC,
});
expect(result.sortGamesBy).toEqual({ field: GameSortField.START_TIME, order: SortDirection.ASC });
});
});
// ── REMOVE_MESSAGES ───────────────────────────────────────────────────────────
describe('REMOVE_MESSAGES', () => {
it('removes messages starting with "name:" up to amount, in reverse scan order', () => {
const msgs = [
makeMessage({ message: 'Alice: hello' }),
makeMessage({ message: 'Bob: hi' }),
makeMessage({ message: 'Alice: world' }),
];
const state = makeRoomsState({ messages: { 1: msgs } });
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
// reverse scan: removes LAST 'Alice:' message first, stops after 1
const remaining = result.messages[1];
expect(remaining).toHaveLength(2);
const aliceMessages = remaining.filter(m => m.message.startsWith('Alice:'));
expect(aliceMessages).toHaveLength(1);
expect(aliceMessages[0].message).toBe('Alice: hello');
});
it('removes up to amount matching messages', () => {
const msgs = [
makeMessage({ message: 'Alice: one' }),
makeMessage({ message: 'Alice: two' }),
makeMessage({ message: 'Alice: three' }),
];
const state = makeRoomsState({ messages: { 1: msgs } });
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 2 });
const remaining = result.messages[1];
expect(remaining).toHaveLength(1);
});
it('stops removing once amount is reached', () => {
const msgs = [
makeMessage({ message: 'Alice: a' }),
makeMessage({ message: 'Alice: b' }),
makeMessage({ message: 'Alice: c' }),
];
const state = makeRoomsState({ messages: { 1: msgs } });
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
expect(result.messages[1]).toHaveLength(2);
});
});
// ── GAME_CREATED ──────────────────────────────────────────────────────────────
describe('GAME_CREATED', () => {
it('returns state unchanged', () => {
const state = makeRoomsState();
const result = roomsReducer(state, { type: Types.GAME_CREATED, roomId: 1 });
expect(result).toBe(state);
});
});
// ── JOINED_GAME ───────────────────────────────────────────────────────────────
describe('JOINED_GAME', () => {
it('sets joinedGameIds[roomId][gameId] = true', () => {
const state = makeRoomsState({ joinedGameIds: {} });
const result = roomsReducer(state, { type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
expect(result.joinedGameIds[1][5]).toBe(true);
});
it('preserves other roomId entries in joinedGameIds', () => {
const state = makeRoomsState({ joinedGameIds: { 2: { 9: true } } });
const result = roomsReducer(state, { type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
expect(result.joinedGameIds[2][9]).toBe(true);
expect(result.joinedGameIds[1][5]).toBe(true);
});
});