mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-22 22:53:55 -07:00
refactor redux data model
This commit is contained in:
parent
ae1bc3da38
commit
0ff391491d
243 changed files with 5212 additions and 5963 deletions
|
|
@ -57,6 +57,20 @@ export default class SortUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/** Non-mutating variant: returns a new sorted array. Intended for use inside selectors. */
|
||||
static sortedByField<T extends object>(arr: readonly T[], sortBy: App.SortBy): T[] {
|
||||
const copy = [...arr];
|
||||
SortUtil.sortByField(copy, sortBy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** Non-mutating variant: returns a new sorted user array. Intended for use inside selectors. */
|
||||
static sortedUsersByField(users: readonly Data.ServerInfo_User[], sortBy: App.SortBy): Data.ServerInfo_User[] {
|
||||
const copy = [...users];
|
||||
SortUtil.sortUsersByField(copy, sortBy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
static toggleSortBy<F extends string>(field: F, sortBy: App.SortBy): { field: F; order: App.SortDirection } {
|
||||
const sameField = field === sortBy.field;
|
||||
const isASC = sortBy.order === App.SortDirection.ASC;
|
||||
|
|
@ -133,19 +147,20 @@ export default class SortUtil {
|
|||
|
||||
private static resolveFieldChain(obj: object, field: string) {
|
||||
const links = field.split('.');
|
||||
|
||||
if (links.length > 1) {
|
||||
return links.reduce((obj, link) => {
|
||||
const parsed = parseInt(link, 10);
|
||||
|
||||
if (parsed.toLocaleString() === 'NaN') {
|
||||
return obj[link];
|
||||
} else {
|
||||
return obj[parsed];
|
||||
}
|
||||
}, obj) || null;
|
||||
} else {
|
||||
if (links.length === 1) {
|
||||
return obj[field];
|
||||
}
|
||||
// Walk nested path; bail to null if we hit a missing intermediate object.
|
||||
// Note: intentionally avoids `|| null` so falsy-but-valid leaf values
|
||||
// (0, '', false) are preserved.
|
||||
let cursor: any = obj;
|
||||
for (const link of links) {
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseInt(link, 10);
|
||||
cursor = Number.isNaN(parsed) ? cursor[link] : cursor[parsed];
|
||||
}
|
||||
return cursor ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { create } from '@bufbuild/protobuf';
|
|||
import { Data, Enriched } from '@app/types';
|
||||
|
||||
describe('normalizeRoomInfo', () => {
|
||||
it('builds gametypeMap from gametypeList and normalises games', () => {
|
||||
it('builds gametypeMap from gametypeList and keys games by gameId', () => {
|
||||
const room = create(Data.ServerInfo_RoomSchema, {
|
||||
roomId: 1,
|
||||
name: 'Lobby',
|
||||
|
|
@ -15,9 +15,11 @@ describe('normalizeRoomInfo', () => {
|
|||
|
||||
const result = normalizeRoomInfo(room);
|
||||
|
||||
expect(result.info).toBe(room);
|
||||
expect(result.gametypeMap).toEqual({ 1: 'Standard' });
|
||||
expect(result.gameList).toHaveLength(1);
|
||||
expect(result.gameList[0].gameType).toBe('Standard');
|
||||
expect(Object.keys(result.games)).toHaveLength(1);
|
||||
expect(result.games[10].gameType).toBe('Standard');
|
||||
expect(result.games[10].info.gameId).toBe(10);
|
||||
expect(result.order).toBe(0);
|
||||
});
|
||||
|
||||
|
|
@ -25,7 +27,21 @@ describe('normalizeRoomInfo', () => {
|
|||
const room = create(Data.ServerInfo_RoomSchema, { roomId: 2, name: 'Empty' });
|
||||
const result = normalizeRoomInfo(room);
|
||||
expect(result.gametypeMap).toEqual({});
|
||||
expect(result.gameList).toEqual([]);
|
||||
expect(result.games).toEqual({});
|
||||
expect(result.users).toEqual({});
|
||||
});
|
||||
|
||||
it('keys users by name', () => {
|
||||
const room = create(Data.ServerInfo_RoomSchema, {
|
||||
roomId: 1,
|
||||
userList: [
|
||||
create(Data.ServerInfo_UserSchema, { name: 'alice' }),
|
||||
create(Data.ServerInfo_UserSchema, { name: 'bob' }),
|
||||
],
|
||||
});
|
||||
const result = normalizeRoomInfo(room);
|
||||
expect(result.users['alice']).toBeDefined();
|
||||
expect(result.users['bob']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -34,6 +50,7 @@ describe('normalizeGameObject', () => {
|
|||
const game = create(Data.ServerInfo_GameSchema, { gameId: 1, gameTypes: [5] });
|
||||
const result = normalizeGameObject(game, { 5: 'Legacy' });
|
||||
expect(result.gameType).toBe('Legacy');
|
||||
expect(result.info).toBe(game);
|
||||
});
|
||||
|
||||
it('returns empty string when no gameTypes', () => {
|
||||
|
|
@ -42,10 +59,11 @@ describe('normalizeGameObject', () => {
|
|||
expect(result.gameType).toBe('');
|
||||
});
|
||||
|
||||
it('fills empty description with empty string', () => {
|
||||
it('stores raw proto on info', () => {
|
||||
const game = create(Data.ServerInfo_GameSchema, { gameId: 3 });
|
||||
const result = normalizeGameObject(game, {});
|
||||
expect(result.description).toBe('');
|
||||
expect(result.info.gameId).toBe(3);
|
||||
expect(result.info.description).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,31 +8,43 @@ export function normalizeGametypeMap(gametypeList: Data.ServerInfo_GameType[]):
|
|||
}, {});
|
||||
}
|
||||
|
||||
/** Flatten room gameTypes into a map object and normalize all games inside. */
|
||||
/**
|
||||
* Build an Enriched.Room (composition shape) from a raw proto. The proto is
|
||||
* stored verbatim on `info` and the repeated collections are normalized into
|
||||
* keyed maps alongside it. `info.gameList`, `info.userList`, `info.gametypeList`
|
||||
* are left as the wire snapshot — callers should always read the normalized
|
||||
* fields, never those.
|
||||
*/
|
||||
export function normalizeRoomInfo(roomInfo: Data.ServerInfo_Room): Enriched.Room {
|
||||
const gametypeMap = normalizeGametypeMap(roomInfo.gametypeList);
|
||||
|
||||
const gameList = roomInfo.gameList.map(
|
||||
(game) => normalizeGameObject(game, gametypeMap),
|
||||
);
|
||||
const games: { [gameId: number]: Enriched.Game } = {};
|
||||
for (const rawGame of roomInfo.gameList) {
|
||||
const normalized = normalizeGameObject(rawGame, gametypeMap);
|
||||
games[normalized.info.gameId] = normalized;
|
||||
}
|
||||
|
||||
const users: { [userName: string]: Data.ServerInfo_User } = {};
|
||||
for (const user of roomInfo.userList) {
|
||||
users[user.name] = user;
|
||||
}
|
||||
|
||||
return {
|
||||
...roomInfo,
|
||||
info: roomInfo,
|
||||
gametypeMap,
|
||||
gameList,
|
||||
order: 0,
|
||||
games,
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
/** Flatten gameTypes[] into a gameType string; fill in default sortable values. */
|
||||
/** Wrap a raw ServerInfo_Game in the composition shape with cached gameType. */
|
||||
export function normalizeGameObject(game: Data.ServerInfo_Game, gametypeMap: Enriched.GametypeMap): Enriched.Game {
|
||||
const { gameTypes, description } = game;
|
||||
const { gameTypes } = game;
|
||||
const hasType = gameTypes && gameTypes.length;
|
||||
|
||||
return {
|
||||
...game,
|
||||
gameType: hasType ? gametypeMap[gameTypes[0]] : '',
|
||||
description: description || '',
|
||||
info: game,
|
||||
gameType: hasType ? (gametypeMap[gameTypes[0]] ?? '') : '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Data } from '@app/types';
|
||||
import { Data, Enriched } from '@app/types';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces';
|
||||
import { GamesState } from '../game.interfaces';
|
||||
|
||||
export function makeCard(overrides: MessageInitShape<typeof Data.ServerInfo_CardSchema> = {}): Data.ServerInfo_Card {
|
||||
return create(Data.ServerInfo_CardSchema, {
|
||||
|
|
@ -45,22 +45,42 @@ export function makeArrow(overrides: MessageInitShape<typeof Data.ServerInfo_Arr
|
|||
startCardId: 1,
|
||||
targetPlayerId: 1,
|
||||
targetZone: 'table',
|
||||
targetCardId: 2,
|
||||
arrowColor: create(Data.colorSchema, { r: 255, g: 0, b: 0, a: 255 }),
|
||||
targetCardId: 2,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
|
||||
type ZoneEntryOverrides = Partial<Enriched.ZoneEntry> & {
|
||||
/**
|
||||
* Convenience for tests: pass an ordered card array and the fixture
|
||||
* materializes it into `{ order, byId }`. If provided, takes precedence
|
||||
* over an explicit `order`/`byId` in the same overrides object.
|
||||
*/
|
||||
cards?: Data.ServerInfo_Card[];
|
||||
};
|
||||
|
||||
export function makeZoneEntry(overrides: ZoneEntryOverrides = {}): Enriched.ZoneEntry {
|
||||
const { cards, order, byId, ...rest } = overrides;
|
||||
let resolvedOrder: number[] = order ?? [];
|
||||
let resolvedById: { [id: number]: Data.ServerInfo_Card } = byId ?? {};
|
||||
if (cards !== undefined) {
|
||||
resolvedOrder = cards.map(c => c.id);
|
||||
resolvedById = {};
|
||||
for (const c of cards) {
|
||||
resolvedById[c.id] = c;
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: 'hand',
|
||||
type: 1,
|
||||
withCoords: false,
|
||||
cardCount: 0,
|
||||
cards: [],
|
||||
order: resolvedOrder,
|
||||
byId: resolvedById,
|
||||
alwaysRevealTopCard: false,
|
||||
alwaysLookAtTopCard: false,
|
||||
...overrides,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +100,7 @@ export function makePlayerProperties(
|
|||
});
|
||||
}
|
||||
|
||||
export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEntry {
|
||||
export function makePlayerEntry(overrides: Partial<Enriched.PlayerEntry> = {}): Enriched.PlayerEntry {
|
||||
return {
|
||||
properties: makePlayerProperties(),
|
||||
deckList: '',
|
||||
|
|
@ -94,11 +114,22 @@ export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEnt
|
|||
};
|
||||
}
|
||||
|
||||
export function makeGameEntry(overrides: Partial<GameEntry> = {}): GameEntry {
|
||||
return {
|
||||
export function makeGameInfo(
|
||||
overrides: MessageInitShape<typeof Data.ServerInfo_GameSchema> = {},
|
||||
): Data.ServerInfo_Game {
|
||||
return create(Data.ServerInfo_GameSchema, {
|
||||
gameId: 1,
|
||||
roomId: 1,
|
||||
description: 'Test Game',
|
||||
gameTypes: [],
|
||||
started: false,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
export function makeGameEntry(overrides: Partial<Enriched.GameEntry> = {}): Enriched.GameEntry {
|
||||
return {
|
||||
info: makeGameInfo(),
|
||||
hostId: 1,
|
||||
localPlayerId: 1,
|
||||
spectator: false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { create } from '@bufbuild/protobuf';
|
||||
import { Data } from '@app/types';
|
||||
import { Actions } from './game.actions';
|
||||
import { Types } from './game.types';
|
||||
import {
|
||||
makeArrow,
|
||||
makeCard,
|
||||
|
|
@ -11,167 +10,189 @@ import {
|
|||
|
||||
describe('Actions', () => {
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
const action = Actions.clearStore();
|
||||
expect(action.type).toBe('games/clearStore');
|
||||
});
|
||||
|
||||
it('gameJoined', () => {
|
||||
const data = create(Data.Event_GameJoinedSchema, { hostId: 1, playerId: 2 });
|
||||
expect(Actions.gameJoined(data)).toEqual({ type: Types.GAME_JOINED, data });
|
||||
const action = Actions.gameJoined({ data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('gameLeft', () => {
|
||||
expect(Actions.gameLeft(2)).toEqual({ type: Types.GAME_LEFT, gameId: 2 });
|
||||
const action = Actions.gameLeft({ gameId: 2 });
|
||||
expect(action.payload.gameId).toBe(2);
|
||||
});
|
||||
|
||||
it('gameClosed', () => {
|
||||
expect(Actions.gameClosed(3)).toEqual({ type: Types.GAME_CLOSED, gameId: 3 });
|
||||
const action = Actions.gameClosed({ gameId: 3 });
|
||||
expect(action.payload.gameId).toBe(3);
|
||||
});
|
||||
|
||||
it('gameHostChanged', () => {
|
||||
expect(Actions.gameHostChanged(1, 7)).toEqual({ type: Types.GAME_HOST_CHANGED, gameId: 1, hostId: 7 });
|
||||
const action = Actions.gameHostChanged({ gameId: 1, hostId: 7 });
|
||||
expect(action.payload).toEqual({ gameId: 1, hostId: 7 });
|
||||
});
|
||||
|
||||
it('gameStateChanged', () => {
|
||||
const data = create(Data.Event_GameStateChangedSchema, {
|
||||
playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0
|
||||
});
|
||||
expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data });
|
||||
const action = Actions.gameStateChanged({ gameId: 1, data });
|
||||
expect(action.payload).toEqual({ gameId: 1, data });
|
||||
});
|
||||
|
||||
it('playerJoined', () => {
|
||||
const props = makePlayerProperties();
|
||||
expect(Actions.playerJoined(1, props)).toEqual({ type: Types.PLAYER_JOINED, gameId: 1, playerProperties: props });
|
||||
const action = Actions.playerJoined({ gameId: 1, playerProperties: props });
|
||||
expect(action.payload.playerProperties).toBe(props);
|
||||
});
|
||||
|
||||
it('playerLeft', () => {
|
||||
expect(Actions.playerLeft(1, 2, 3)).toEqual({ type: Types.PLAYER_LEFT, gameId: 1, playerId: 2, reason: 3 });
|
||||
const action = Actions.playerLeft({ gameId: 1, playerId: 2 });
|
||||
expect(action.payload).toEqual({ gameId: 1, playerId: 2 });
|
||||
});
|
||||
|
||||
it('playerPropertiesChanged', () => {
|
||||
const props = makePlayerProperties();
|
||||
expect(Actions.playerPropertiesChanged(1, 2, props)).toEqual({
|
||||
type: Types.PLAYER_PROPERTIES_CHANGED,
|
||||
gameId: 1,
|
||||
playerId: 2,
|
||||
properties: props,
|
||||
});
|
||||
const action = Actions.playerPropertiesChanged({ gameId: 1, playerId: 2, properties: props });
|
||||
expect(action.payload.properties).toBe(props);
|
||||
});
|
||||
|
||||
it('kicked', () => {
|
||||
expect(Actions.kicked(1)).toEqual({ type: Types.KICKED, gameId: 1 });
|
||||
const action = Actions.kicked({ gameId: 1 });
|
||||
expect(action.payload.gameId).toBe(1);
|
||||
});
|
||||
|
||||
it('cardMoved', () => {
|
||||
const data = create(Data.Event_MoveCardSchema, { cardId: 1 });
|
||||
expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardMoved({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload).toEqual({ gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardFlipped', () => {
|
||||
const data = create(Data.Event_FlipCardSchema, { cardId: 1 });
|
||||
expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardFlipped({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardDestroyed', () => {
|
||||
const data = create(Data.Event_DestroyCardSchema, { cardId: 1 });
|
||||
expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardDestroyed({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardAttached', () => {
|
||||
const data = create(Data.Event_AttachCardSchema, { cardId: 1 });
|
||||
expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardAttached({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('tokenCreated', () => {
|
||||
const data = create(Data.Event_CreateTokenSchema, { cardId: 1 });
|
||||
expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.tokenCreated({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardAttrChanged', () => {
|
||||
const data = create(Data.Event_SetCardAttrSchema, { cardId: 1 });
|
||||
expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardAttrChanged({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardCounterChanged', () => {
|
||||
const data = create(Data.Event_SetCardCounterSchema, { cardId: 1 });
|
||||
expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardCounterChanged({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('arrowCreated', () => {
|
||||
const arrow = makeArrow();
|
||||
const data = create(Data.Event_CreateArrowSchema, { arrowInfo: arrow });
|
||||
expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.arrowCreated({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('arrowDeleted', () => {
|
||||
const data = create(Data.Event_DeleteArrowSchema, { arrowId: 3 });
|
||||
expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.arrowDeleted({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('counterCreated', () => {
|
||||
const counter = makeCounter();
|
||||
const data = create(Data.Event_CreateCounterSchema, { counterInfo: counter });
|
||||
expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.counterCreated({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('counterSet', () => {
|
||||
const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 10 });
|
||||
expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.counterSet({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('counterDeleted', () => {
|
||||
const data = create(Data.Event_DelCounterSchema, { counterId: 1 });
|
||||
expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.counterDeleted({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardsDrawn', () => {
|
||||
const card = makeCard();
|
||||
const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [card] });
|
||||
expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardsDrawn({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('cardsRevealed', () => {
|
||||
const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] });
|
||||
expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.cardsRevealed({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('zoneShuffled', () => {
|
||||
const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 });
|
||||
expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.zoneShuffled({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('dieRolled', () => {
|
||||
const data = create(Data.Event_RollDieSchema, { sides: 6, value: 4, values: [4] });
|
||||
expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.dieRolled({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('activePlayerSet', () => {
|
||||
expect(Actions.activePlayerSet(1, 3)).toEqual({ type: Types.ACTIVE_PLAYER_SET, gameId: 1, activePlayerId: 3 });
|
||||
const action = Actions.activePlayerSet({ gameId: 1, activePlayerId: 3 });
|
||||
expect(action.payload).toEqual({ gameId: 1, activePlayerId: 3 });
|
||||
});
|
||||
|
||||
it('activePhaseSet', () => {
|
||||
expect(Actions.activePhaseSet(1, 2)).toEqual({ type: Types.ACTIVE_PHASE_SET, gameId: 1, phase: 2 });
|
||||
const action = Actions.activePhaseSet({ gameId: 1, phase: 2 });
|
||||
expect(action.payload).toEqual({ gameId: 1, phase: 2 });
|
||||
});
|
||||
|
||||
it('turnReversed', () => {
|
||||
expect(Actions.turnReversed(1, true)).toEqual({ type: Types.TURN_REVERSED, gameId: 1, reversed: true });
|
||||
const action = Actions.turnReversed({ gameId: 1, reversed: true });
|
||||
expect(action.payload).toEqual({ gameId: 1, reversed: true });
|
||||
});
|
||||
|
||||
it('zoneDumped', () => {
|
||||
const data = create(Data.Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false });
|
||||
expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data });
|
||||
const action = Actions.zoneDumped({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('zonePropertiesChanged', () => {
|
||||
const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false });
|
||||
expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({
|
||||
type: Types.ZONE_PROPERTIES_CHANGED,
|
||||
gameId: 1,
|
||||
playerId: 2,
|
||||
data,
|
||||
});
|
||||
const action = Actions.zonePropertiesChanged({ gameId: 1, playerId: 2, data });
|
||||
expect(action.payload.data).toBe(data);
|
||||
});
|
||||
|
||||
it('gameSay', () => {
|
||||
expect(Actions.gameSay(1, 2, 'hello')).toEqual({ type: Types.GAME_SAY, gameId: 1, playerId: 2, message: 'hello' });
|
||||
const action = Actions.gameSay({ gameId: 1, playerId: 2, message: 'hello' });
|
||||
expect(action.payload).toEqual({ gameId: 1, playerId: 2, message: 'hello' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,213 +1,5 @@
|
|||
import type { Data } from '@app/types';
|
||||
import { Types } from './game.types';
|
||||
import { gamesSlice } from './game.reducer';
|
||||
|
||||
export const Actions = {
|
||||
clearStore: () => ({
|
||||
type: Types.CLEAR_STORE,
|
||||
}),
|
||||
|
||||
gameJoined: (data: Data.Event_GameJoined) => ({
|
||||
type: Types.GAME_JOINED,
|
||||
data,
|
||||
}),
|
||||
|
||||
gameLeft: (gameId: number) => ({
|
||||
type: Types.GAME_LEFT,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
gameClosed: (gameId: number) => ({
|
||||
type: Types.GAME_CLOSED,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
gameHostChanged: (gameId: number, hostId: number) => ({
|
||||
type: Types.GAME_HOST_CHANGED,
|
||||
gameId,
|
||||
hostId,
|
||||
}),
|
||||
|
||||
gameStateChanged: (gameId: number, data: Data.Event_GameStateChanged) => ({
|
||||
type: Types.GAME_STATE_CHANGED,
|
||||
gameId,
|
||||
data,
|
||||
}),
|
||||
|
||||
playerJoined: (gameId: number, playerProperties: Data.ServerInfo_PlayerProperties) => ({
|
||||
type: Types.PLAYER_JOINED,
|
||||
gameId,
|
||||
playerProperties,
|
||||
}),
|
||||
|
||||
playerLeft: (gameId: number, playerId: number, reason: number) => ({
|
||||
type: Types.PLAYER_LEFT,
|
||||
gameId,
|
||||
playerId,
|
||||
reason,
|
||||
}),
|
||||
|
||||
playerPropertiesChanged: (gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties) => ({
|
||||
type: Types.PLAYER_PROPERTIES_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
properties,
|
||||
}),
|
||||
|
||||
kicked: (gameId: number) => ({
|
||||
type: Types.KICKED,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
cardMoved: (gameId: number, playerId: number, data: Data.Event_MoveCard) => ({
|
||||
type: Types.CARD_MOVED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardFlipped: (gameId: number, playerId: number, data: Data.Event_FlipCard) => ({
|
||||
type: Types.CARD_FLIPPED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardDestroyed: (gameId: number, playerId: number, data: Data.Event_DestroyCard) => ({
|
||||
type: Types.CARD_DESTROYED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardAttached: (gameId: number, playerId: number, data: Data.Event_AttachCard) => ({
|
||||
type: Types.CARD_ATTACHED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
tokenCreated: (gameId: number, playerId: number, data: Data.Event_CreateToken) => ({
|
||||
type: Types.TOKEN_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardAttrChanged: (gameId: number, playerId: number, data: Data.Event_SetCardAttr) => ({
|
||||
type: Types.CARD_ATTR_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardCounterChanged: (gameId: number, playerId: number, data: Data.Event_SetCardCounter) => ({
|
||||
type: Types.CARD_COUNTER_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
arrowCreated: (gameId: number, playerId: number, data: Data.Event_CreateArrow) => ({
|
||||
type: Types.ARROW_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
arrowDeleted: (gameId: number, playerId: number, data: Data.Event_DeleteArrow) => ({
|
||||
type: Types.ARROW_DELETED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterCreated: (gameId: number, playerId: number, data: Data.Event_CreateCounter) => ({
|
||||
type: Types.COUNTER_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterSet: (gameId: number, playerId: number, data: Data.Event_SetCounter) => ({
|
||||
type: Types.COUNTER_SET,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterDeleted: (gameId: number, playerId: number, data: Data.Event_DelCounter) => ({
|
||||
type: Types.COUNTER_DELETED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardsDrawn: (gameId: number, playerId: number, data: Data.Event_DrawCards) => ({
|
||||
type: Types.CARDS_DRAWN,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardsRevealed: (gameId: number, playerId: number, data: Data.Event_RevealCards) => ({
|
||||
type: Types.CARDS_REVEALED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
zoneShuffled: (gameId: number, playerId: number, data: Data.Event_Shuffle) => ({
|
||||
type: Types.ZONE_SHUFFLED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
dieRolled: (gameId: number, playerId: number, data: Data.Event_RollDie) => ({
|
||||
type: Types.DIE_ROLLED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
activePlayerSet: (gameId: number, activePlayerId: number) => ({
|
||||
type: Types.ACTIVE_PLAYER_SET,
|
||||
gameId,
|
||||
activePlayerId,
|
||||
}),
|
||||
|
||||
activePhaseSet: (gameId: number, phase: number) => ({
|
||||
type: Types.ACTIVE_PHASE_SET,
|
||||
gameId,
|
||||
phase,
|
||||
}),
|
||||
|
||||
turnReversed: (gameId: number, reversed: boolean) => ({
|
||||
type: Types.TURN_REVERSED,
|
||||
gameId,
|
||||
reversed,
|
||||
}),
|
||||
|
||||
zoneDumped: (gameId: number, playerId: number, data: Data.Event_DumpZone) => ({
|
||||
type: Types.ZONE_DUMPED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
zonePropertiesChanged: (gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties) => ({
|
||||
type: Types.ZONE_PROPERTIES_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
gameSay: (gameId: number, playerId: number, message: string) => ({
|
||||
type: Types.GAME_SAY,
|
||||
gameId,
|
||||
playerId,
|
||||
message,
|
||||
}),
|
||||
};
|
||||
export const Actions = gamesSlice.actions;
|
||||
|
||||
export type GameAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// Use `vi.hoisted` so the mocked `store.dispatch` reference stays stable across
|
||||
// re-runs of the factory under `isolate: false`. See rooms.dispatch.spec.ts for
|
||||
// the same pattern and rationale.
|
||||
const { mockDispatch } = vi.hoisted(() => ({ mockDispatch: vi.fn() }));
|
||||
vi.mock('../store', () => ({ store: { dispatch: mockDispatch } }));
|
||||
|
||||
|
|
@ -28,22 +25,22 @@ describe('Dispatch', () => {
|
|||
it('gameJoined dispatches Actions.gameJoined()', () => {
|
||||
const data = create(Data.Event_GameJoinedSchema, { hostId: 1, playerId: 2 });
|
||||
Dispatch.gameJoined(data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameJoined(data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameJoined({ data }));
|
||||
});
|
||||
|
||||
it('gameLeft dispatches Actions.gameLeft()', () => {
|
||||
Dispatch.gameLeft(2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameLeft(2));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameLeft({ gameId: 2 }));
|
||||
});
|
||||
|
||||
it('gameClosed dispatches Actions.gameClosed()', () => {
|
||||
Dispatch.gameClosed(3);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameClosed(3));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameClosed({ gameId: 3 }));
|
||||
});
|
||||
|
||||
it('gameHostChanged dispatches Actions.gameHostChanged()', () => {
|
||||
Dispatch.gameHostChanged(1, 7);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameHostChanged(1, 7));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameHostChanged({ gameId: 1, hostId: 7 }));
|
||||
});
|
||||
|
||||
it('gameStateChanged dispatches Actions.gameStateChanged()', () => {
|
||||
|
|
@ -51,156 +48,156 @@ describe('Dispatch', () => {
|
|||
playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0
|
||||
});
|
||||
Dispatch.gameStateChanged(1, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameStateChanged(1, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameStateChanged({ gameId: 1, data }));
|
||||
});
|
||||
|
||||
it('playerJoined dispatches Actions.playerJoined()', () => {
|
||||
const props = makePlayerProperties();
|
||||
Dispatch.playerJoined(1, props);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerJoined(1, props));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerJoined({ gameId: 1, playerProperties: props }));
|
||||
});
|
||||
|
||||
it('playerLeft dispatches Actions.playerLeft()', () => {
|
||||
Dispatch.playerLeft(1, 2, 3);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerLeft(1, 2, 3));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerLeft({ gameId: 1, playerId: 2 }));
|
||||
});
|
||||
|
||||
it('playerPropertiesChanged dispatches Actions.playerPropertiesChanged()', () => {
|
||||
const props = makePlayerProperties();
|
||||
Dispatch.playerPropertiesChanged(1, 2, props);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerPropertiesChanged(1, 2, props));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.playerPropertiesChanged({ gameId: 1, playerId: 2, properties: props }));
|
||||
});
|
||||
|
||||
it('kicked dispatches Actions.kicked()', () => {
|
||||
Dispatch.kicked(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.kicked(1));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.kicked({ gameId: 1 }));
|
||||
});
|
||||
|
||||
it('cardMoved dispatches Actions.cardMoved()', () => {
|
||||
const data = create(Data.Event_MoveCardSchema, { cardId: 1 });
|
||||
Dispatch.cardMoved(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardMoved({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardFlipped dispatches Actions.cardFlipped()', () => {
|
||||
const data = create(Data.Event_FlipCardSchema, { cardId: 1 });
|
||||
Dispatch.cardFlipped(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardFlipped({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardDestroyed dispatches Actions.cardDestroyed()', () => {
|
||||
const data = create(Data.Event_DestroyCardSchema, { cardId: 1 });
|
||||
Dispatch.cardDestroyed(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardDestroyed({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardAttached dispatches Actions.cardAttached()', () => {
|
||||
const data = create(Data.Event_AttachCardSchema, { cardId: 1 });
|
||||
Dispatch.cardAttached(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardAttached({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('tokenCreated dispatches Actions.tokenCreated()', () => {
|
||||
const data = create(Data.Event_CreateTokenSchema, { cardId: 1 });
|
||||
Dispatch.tokenCreated(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.tokenCreated({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => {
|
||||
const data = create(Data.Event_SetCardAttrSchema, { cardId: 1 });
|
||||
Dispatch.cardAttrChanged(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardAttrChanged({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => {
|
||||
const data = create(Data.Event_SetCardCounterSchema, { cardId: 1 });
|
||||
Dispatch.cardCounterChanged(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardCounterChanged({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('arrowCreated dispatches Actions.arrowCreated()', () => {
|
||||
const data = create(Data.Event_CreateArrowSchema, { arrowInfo: makeArrow() });
|
||||
Dispatch.arrowCreated(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.arrowCreated({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('arrowDeleted dispatches Actions.arrowDeleted()', () => {
|
||||
const data = create(Data.Event_DeleteArrowSchema, { arrowId: 3 });
|
||||
Dispatch.arrowDeleted(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.arrowDeleted({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('counterCreated dispatches Actions.counterCreated()', () => {
|
||||
const data = create(Data.Event_CreateCounterSchema, { counterInfo: makeCounter() });
|
||||
Dispatch.counterCreated(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterCreated({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('counterSet dispatches Actions.counterSet()', () => {
|
||||
const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 10 });
|
||||
Dispatch.counterSet(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterSet({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('counterDeleted dispatches Actions.counterDeleted()', () => {
|
||||
const data = create(Data.Event_DelCounterSchema, { counterId: 1 });
|
||||
Dispatch.counterDeleted(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.counterDeleted({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardsDrawn dispatches Actions.cardsDrawn()', () => {
|
||||
const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [makeCard()] });
|
||||
Dispatch.cardsDrawn(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardsDrawn({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('cardsRevealed dispatches Actions.cardsRevealed()', () => {
|
||||
const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] });
|
||||
Dispatch.cardsRevealed(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.cardsRevealed({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('zoneShuffled dispatches Actions.zoneShuffled()', () => {
|
||||
const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 });
|
||||
Dispatch.zoneShuffled(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zoneShuffled({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('dieRolled dispatches Actions.dieRolled()', () => {
|
||||
const data = create(Data.Event_RollDieSchema, { sides: 6, value: 4, values: [4] });
|
||||
Dispatch.dieRolled(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.dieRolled({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('activePlayerSet dispatches Actions.activePlayerSet()', () => {
|
||||
Dispatch.activePlayerSet(1, 3);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.activePlayerSet(1, 3));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.activePlayerSet({ gameId: 1, activePlayerId: 3 }));
|
||||
});
|
||||
|
||||
it('activePhaseSet dispatches Actions.activePhaseSet()', () => {
|
||||
Dispatch.activePhaseSet(1, 2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.activePhaseSet(1, 2));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.activePhaseSet({ gameId: 1, phase: 2 }));
|
||||
});
|
||||
|
||||
it('turnReversed dispatches Actions.turnReversed()', () => {
|
||||
Dispatch.turnReversed(1, true);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.turnReversed(1, true));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.turnReversed({ gameId: 1, reversed: true }));
|
||||
});
|
||||
|
||||
it('zoneDumped dispatches Actions.zoneDumped()', () => {
|
||||
const data = create(Data.Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false });
|
||||
Dispatch.zoneDumped(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zoneDumped({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => {
|
||||
const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false });
|
||||
Dispatch.zonePropertiesChanged(1, 2, data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged({ gameId: 1, playerId: 2, data }));
|
||||
});
|
||||
|
||||
it('gameSay dispatches Actions.gameSay()', () => {
|
||||
Dispatch.gameSay(1, 2, 'gg wp');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameSay(1, 2, 'gg wp'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameSay({ gameId: 1, playerId: 2, message: 'gg wp' }));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,126 +8,126 @@ export const Dispatch = {
|
|||
},
|
||||
|
||||
gameJoined: (data: Data.Event_GameJoined) => {
|
||||
store.dispatch(Actions.gameJoined(data));
|
||||
store.dispatch(Actions.gameJoined({ data }));
|
||||
},
|
||||
|
||||
gameLeft: (gameId: number) => {
|
||||
store.dispatch(Actions.gameLeft(gameId));
|
||||
store.dispatch(Actions.gameLeft({ gameId }));
|
||||
},
|
||||
|
||||
gameClosed: (gameId: number) => {
|
||||
store.dispatch(Actions.gameClosed(gameId));
|
||||
store.dispatch(Actions.gameClosed({ gameId }));
|
||||
},
|
||||
|
||||
gameHostChanged: (gameId: number, hostId: number) => {
|
||||
store.dispatch(Actions.gameHostChanged(gameId, hostId));
|
||||
store.dispatch(Actions.gameHostChanged({ gameId, hostId }));
|
||||
},
|
||||
|
||||
gameStateChanged: (gameId: number, data: Data.Event_GameStateChanged) => {
|
||||
store.dispatch(Actions.gameStateChanged(gameId, data));
|
||||
store.dispatch(Actions.gameStateChanged({ gameId, data }));
|
||||
},
|
||||
|
||||
playerJoined: (gameId: number, playerProperties: Data.ServerInfo_PlayerProperties) => {
|
||||
store.dispatch(Actions.playerJoined(gameId, playerProperties));
|
||||
store.dispatch(Actions.playerJoined({ gameId, playerProperties }));
|
||||
},
|
||||
|
||||
playerLeft: (gameId: number, playerId: number, reason: number) => {
|
||||
store.dispatch(Actions.playerLeft(gameId, playerId, reason));
|
||||
playerLeft: (gameId: number, playerId: number, _reason: number) => {
|
||||
store.dispatch(Actions.playerLeft({ gameId, playerId }));
|
||||
},
|
||||
|
||||
playerPropertiesChanged: (gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties) => {
|
||||
store.dispatch(Actions.playerPropertiesChanged(gameId, playerId, properties));
|
||||
store.dispatch(Actions.playerPropertiesChanged({ gameId, playerId, properties }));
|
||||
},
|
||||
|
||||
kicked: (gameId: number) => {
|
||||
store.dispatch(Actions.kicked(gameId));
|
||||
store.dispatch(Actions.kicked({ gameId }));
|
||||
},
|
||||
|
||||
cardMoved: (gameId: number, playerId: number, data: Data.Event_MoveCard) => {
|
||||
store.dispatch(Actions.cardMoved(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardMoved({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardFlipped: (gameId: number, playerId: number, data: Data.Event_FlipCard) => {
|
||||
store.dispatch(Actions.cardFlipped(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardFlipped({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardDestroyed: (gameId: number, playerId: number, data: Data.Event_DestroyCard) => {
|
||||
store.dispatch(Actions.cardDestroyed(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardDestroyed({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardAttached: (gameId: number, playerId: number, data: Data.Event_AttachCard) => {
|
||||
store.dispatch(Actions.cardAttached(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardAttached({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
tokenCreated: (gameId: number, playerId: number, data: Data.Event_CreateToken) => {
|
||||
store.dispatch(Actions.tokenCreated(gameId, playerId, data));
|
||||
store.dispatch(Actions.tokenCreated({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardAttrChanged: (gameId: number, playerId: number, data: Data.Event_SetCardAttr) => {
|
||||
store.dispatch(Actions.cardAttrChanged(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardAttrChanged({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardCounterChanged: (gameId: number, playerId: number, data: Data.Event_SetCardCounter) => {
|
||||
store.dispatch(Actions.cardCounterChanged(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardCounterChanged({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
arrowCreated: (gameId: number, playerId: number, data: Data.Event_CreateArrow) => {
|
||||
store.dispatch(Actions.arrowCreated(gameId, playerId, data));
|
||||
store.dispatch(Actions.arrowCreated({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
arrowDeleted: (gameId: number, playerId: number, data: Data.Event_DeleteArrow) => {
|
||||
store.dispatch(Actions.arrowDeleted(gameId, playerId, data));
|
||||
store.dispatch(Actions.arrowDeleted({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
counterCreated: (gameId: number, playerId: number, data: Data.Event_CreateCounter) => {
|
||||
store.dispatch(Actions.counterCreated(gameId, playerId, data));
|
||||
store.dispatch(Actions.counterCreated({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
counterSet: (gameId: number, playerId: number, data: Data.Event_SetCounter) => {
|
||||
store.dispatch(Actions.counterSet(gameId, playerId, data));
|
||||
store.dispatch(Actions.counterSet({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
counterDeleted: (gameId: number, playerId: number, data: Data.Event_DelCounter) => {
|
||||
store.dispatch(Actions.counterDeleted(gameId, playerId, data));
|
||||
store.dispatch(Actions.counterDeleted({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardsDrawn: (gameId: number, playerId: number, data: Data.Event_DrawCards) => {
|
||||
store.dispatch(Actions.cardsDrawn(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardsDrawn({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
cardsRevealed: (gameId: number, playerId: number, data: Data.Event_RevealCards) => {
|
||||
store.dispatch(Actions.cardsRevealed(gameId, playerId, data));
|
||||
store.dispatch(Actions.cardsRevealed({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
zoneShuffled: (gameId: number, playerId: number, data: Data.Event_Shuffle) => {
|
||||
store.dispatch(Actions.zoneShuffled(gameId, playerId, data));
|
||||
store.dispatch(Actions.zoneShuffled({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
dieRolled: (gameId: number, playerId: number, data: Data.Event_RollDie) => {
|
||||
store.dispatch(Actions.dieRolled(gameId, playerId, data));
|
||||
store.dispatch(Actions.dieRolled({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
activePlayerSet: (gameId: number, activePlayerId: number) => {
|
||||
store.dispatch(Actions.activePlayerSet(gameId, activePlayerId));
|
||||
store.dispatch(Actions.activePlayerSet({ gameId, activePlayerId }));
|
||||
},
|
||||
|
||||
activePhaseSet: (gameId: number, phase: number) => {
|
||||
store.dispatch(Actions.activePhaseSet(gameId, phase));
|
||||
store.dispatch(Actions.activePhaseSet({ gameId, phase }));
|
||||
},
|
||||
|
||||
turnReversed: (gameId: number, reversed: boolean) => {
|
||||
store.dispatch(Actions.turnReversed(gameId, reversed));
|
||||
store.dispatch(Actions.turnReversed({ gameId, reversed }));
|
||||
},
|
||||
|
||||
zoneDumped: (gameId: number, playerId: number, data: Data.Event_DumpZone) => {
|
||||
store.dispatch(Actions.zoneDumped(gameId, playerId, data));
|
||||
store.dispatch(Actions.zoneDumped({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
zonePropertiesChanged: (gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties) => {
|
||||
store.dispatch(Actions.zonePropertiesChanged(gameId, playerId, data));
|
||||
store.dispatch(Actions.zonePropertiesChanged({ gameId, playerId, data }));
|
||||
},
|
||||
|
||||
gameSay: (gameId: number, playerId: number, message: string) => {
|
||||
store.dispatch(Actions.gameSay(gameId, playerId, message));
|
||||
store.dispatch(Actions.gameSay({ gameId, playerId, message }));
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,60 +1,5 @@
|
|||
import type { Data } from '@app/types';
|
||||
import type { Enriched } from '@app/types';
|
||||
|
||||
export interface GamesState {
|
||||
games: { [gameId: number]: GameEntry };
|
||||
}
|
||||
|
||||
/**
|
||||
* Full runtime state for a single active game (played or spectated).
|
||||
* Keyed by gameId in GamesState so multiple concurrent games are supported.
|
||||
*/
|
||||
export interface GameEntry {
|
||||
gameId: number;
|
||||
roomId: number;
|
||||
description: string;
|
||||
hostId: number;
|
||||
/** The playerId assigned to the local user in this game. */
|
||||
localPlayerId: number;
|
||||
spectator: boolean;
|
||||
judge: boolean;
|
||||
resuming: boolean;
|
||||
started: boolean;
|
||||
activePlayerId: number;
|
||||
activePhase: number;
|
||||
secondsElapsed: number;
|
||||
reversed: boolean;
|
||||
players: { [playerId: number]: PlayerEntry };
|
||||
messages: GameMessage[];
|
||||
}
|
||||
|
||||
/** Normalized from ServerInfo_Player — keyed collections for O(1) lookup. */
|
||||
export interface PlayerEntry {
|
||||
properties: Data.ServerInfo_PlayerProperties;
|
||||
deckList: string;
|
||||
/** Zones keyed by zone name (e.g. "hand", "deck", "table"). */
|
||||
zones: { [zoneName: string]: ZoneEntry };
|
||||
/** Player-level counters (e.g. life) keyed by counter id. */
|
||||
counters: { [counterId: number]: Data.ServerInfo_Counter };
|
||||
/** Arrows keyed by arrow id. */
|
||||
arrows: { [arrowId: number]: Data.ServerInfo_Arrow };
|
||||
}
|
||||
|
||||
/** Normalized from ServerInfo_Zone — card list is an ordered array matching proto. */
|
||||
export interface ZoneEntry {
|
||||
name: string;
|
||||
/** ZoneType enum value (0=Private, 1=Public, 2=Hidden). */
|
||||
type: number;
|
||||
withCoords: boolean;
|
||||
/** Authoritative card count (used for hidden zones where cardList may be empty). */
|
||||
cardCount: number;
|
||||
/** Ordered card list; may be empty for hidden zones with no dump active. */
|
||||
cards: Data.ServerInfo_Card[];
|
||||
alwaysRevealTopCard: boolean;
|
||||
alwaysLookAtTopCard: boolean;
|
||||
}
|
||||
|
||||
export interface GameMessage {
|
||||
playerId: number;
|
||||
message: string;
|
||||
timeReceived: number;
|
||||
games: { [gameId: number]: Enriched.GameEntry };
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,85 +1,31 @@
|
|||
import { Data } from '@app/types';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Data, Enriched } from '@app/types';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { GameAction } from './game.actions';
|
||||
import { GameEntry, GameMessage, GamesState, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||
import { Types } from './game.types';
|
||||
import { GamesState } from './game.interfaces';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
export const MAX_GAME_MESSAGES = 1000;
|
||||
|
||||
function updateGame(state: GamesState, gameId: number, updates: Partial<GameEntry>): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
games: { ...state.games, [gameId]: { ...game, ...updates } },
|
||||
};
|
||||
}
|
||||
|
||||
function updatePlayer(
|
||||
state: GamesState,
|
||||
gameId: number,
|
||||
playerId: number,
|
||||
updates: Partial<PlayerEntry>
|
||||
): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updateGame(state, gameId, {
|
||||
players: { ...game.players, [playerId]: { ...player, ...updates } },
|
||||
});
|
||||
}
|
||||
|
||||
function updateZone(
|
||||
state: GamesState,
|
||||
gameId: number,
|
||||
playerId: number,
|
||||
zoneName: string,
|
||||
updates: Partial<ZoneEntry>
|
||||
): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
zones: { ...player.zones, [zoneName]: { ...zone, ...updates } },
|
||||
});
|
||||
}
|
||||
|
||||
function removeGame(state: GamesState, gameId: number): GamesState {
|
||||
const games = { ...state.games };
|
||||
delete games[gameId];
|
||||
return { ...state, games };
|
||||
}
|
||||
|
||||
/** Converts the proto PlayerInfo[] array into the keyed PlayerEntry map used in the store. */
|
||||
function normalizePlayers(playerList: Data.ServerInfo_Player[]): { [playerId: number]: PlayerEntry } {
|
||||
const players: { [playerId: number]: PlayerEntry } = {};
|
||||
/** Converts the proto ServerInfo_Player[] array into the keyed PlayerEntry map. */
|
||||
function normalizePlayers(playerList: Data.ServerInfo_Player[]): { [playerId: number]: Enriched.PlayerEntry } {
|
||||
const players: { [playerId: number]: Enriched.PlayerEntry } = {};
|
||||
for (const player of playerList) {
|
||||
const playerId = player.properties.playerId;
|
||||
|
||||
const zones: { [zoneName: string]: ZoneEntry } = {};
|
||||
const zones: { [zoneName: string]: Enriched.ZoneEntry } = {};
|
||||
for (const zone of player.zoneList) {
|
||||
const order: number[] = [];
|
||||
const byId: { [id: number]: Data.ServerInfo_Card } = {};
|
||||
for (const card of zone.cardList) {
|
||||
order.push(card.id);
|
||||
byId[card.id] = card;
|
||||
}
|
||||
zones[zone.name] = {
|
||||
name: zone.name,
|
||||
type: zone.type,
|
||||
withCoords: zone.withCoords,
|
||||
cardCount: zone.cardCount,
|
||||
cards: [...zone.cardList],
|
||||
order,
|
||||
byId,
|
||||
alwaysRevealTopCard: zone.alwaysRevealTopCard,
|
||||
alwaysLookAtTopCard: zone.alwaysLookAtTopCard,
|
||||
};
|
||||
|
|
@ -115,50 +61,29 @@ function buildEmptyCard(
|
|||
providerId: string
|
||||
): Data.ServerInfo_Card {
|
||||
return create(Data.ServerInfo_CardSchema, {
|
||||
id,
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
tapped: false,
|
||||
attacking: false,
|
||||
color: '',
|
||||
pt: '',
|
||||
annotation: '',
|
||||
destroyOnZoneChange: false,
|
||||
doesntUntap: false,
|
||||
counterList: [],
|
||||
attachPlayerId: -1,
|
||||
attachZone: '',
|
||||
attachCardId: -1,
|
||||
providerId,
|
||||
id, name, x, y, faceDown,
|
||||
tapped: false, attacking: false, color: '', pt: '', annotation: '',
|
||||
destroyOnZoneChange: false, doesntUntap: false, counterList: [],
|
||||
attachPlayerId: -1, attachZone: '', attachCardId: -1, providerId,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Initial state ─────────────────────────────────────────────────────────────
|
||||
const initialState: GamesState = { games: {} };
|
||||
|
||||
const initialState: GamesState = {
|
||||
games: {},
|
||||
};
|
||||
export const gamesSlice = createSlice({
|
||||
name: 'games',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearStore: () => initialState,
|
||||
|
||||
// ── Reducer ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const gamesReducer = (state: GamesState = initialState, action: GameAction): GamesState => {
|
||||
switch (action.type) {
|
||||
case Types.CLEAR_STORE: {
|
||||
return initialState;
|
||||
}
|
||||
|
||||
case Types.GAME_JOINED: {
|
||||
const { data } = action;
|
||||
gameJoined: (state, action: PayloadAction<{ data: Data.Event_GameJoined }>) => {
|
||||
const { data } = action.payload;
|
||||
const gameInfo = data.gameInfo;
|
||||
if (!gameInfo) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
const gameEntry: GameEntry = {
|
||||
gameId: gameInfo.gameId,
|
||||
roomId: gameInfo.roomId,
|
||||
description: gameInfo.description,
|
||||
state.games[gameInfo.gameId] = {
|
||||
info: gameInfo,
|
||||
hostId: data.hostId,
|
||||
localPlayerId: data.playerId,
|
||||
spectator: data.spectator,
|
||||
|
|
@ -172,574 +97,391 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio
|
|||
players: {},
|
||||
messages: [],
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
games: { ...state.games, [gameEntry.gameId]: gameEntry },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
case Types.GAME_LEFT:
|
||||
case Types.GAME_CLOSED:
|
||||
case Types.KICKED: {
|
||||
return removeGame(state, action.gameId);
|
||||
}
|
||||
gameLeft: (state, action: PayloadAction<{ gameId: number }>) => {
|
||||
delete state.games[action.payload.gameId];
|
||||
},
|
||||
|
||||
case Types.GAME_HOST_CHANGED: {
|
||||
return updateGame(state, action.gameId, { hostId: action.hostId });
|
||||
}
|
||||
gameClosed: (state, action: PayloadAction<{ gameId: number }>) => {
|
||||
delete state.games[action.payload.gameId];
|
||||
},
|
||||
|
||||
case Types.GAME_STATE_CHANGED: {
|
||||
const { gameId, data } = action;
|
||||
kicked: (state, action: PayloadAction<{ gameId: number }>) => {
|
||||
delete state.games[action.payload.gameId];
|
||||
},
|
||||
|
||||
gameHostChanged: (state, action: PayloadAction<{ gameId: number; hostId: number }>) => {
|
||||
const { gameId, hostId } = action.payload;
|
||||
const game = state.games[gameId];
|
||||
if (game) {
|
||||
game.hostId = hostId;
|
||||
}
|
||||
},
|
||||
|
||||
gameStateChanged: (state, action: PayloadAction<{ gameId: number; data: Data.Event_GameStateChanged }>) => {
|
||||
const { gameId, data } = action.payload;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: Partial<GameEntry> = {};
|
||||
if (data.playerList?.length > 0) {
|
||||
updates.players = normalizePlayers(data.playerList);
|
||||
game.players = normalizePlayers(data.playerList);
|
||||
}
|
||||
if (data.gameStarted !== undefined && data.gameStarted !== null) {
|
||||
updates.started = data.gameStarted;
|
||||
game.started = data.gameStarted;
|
||||
}
|
||||
if (data.activePlayerId !== undefined && data.activePlayerId !== null) {
|
||||
updates.activePlayerId = data.activePlayerId;
|
||||
game.activePlayerId = data.activePlayerId;
|
||||
}
|
||||
if (data.activePhase !== undefined && data.activePhase !== null) {
|
||||
updates.activePhase = data.activePhase;
|
||||
game.activePhase = data.activePhase;
|
||||
}
|
||||
if (data.secondsElapsed !== undefined) {
|
||||
updates.secondsElapsed = data.secondsElapsed;
|
||||
game.secondsElapsed = data.secondsElapsed;
|
||||
}
|
||||
return updateGame(state, gameId, updates);
|
||||
}
|
||||
},
|
||||
|
||||
case Types.PLAYER_JOINED: {
|
||||
const { gameId, playerProperties } = action;
|
||||
playerJoined: (state, action: PayloadAction<{ gameId: number; playerProperties: Data.ServerInfo_PlayerProperties }>) => {
|
||||
const { gameId, playerProperties } = action.payload;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
const newPlayer: PlayerEntry = {
|
||||
game.players[playerProperties.playerId] = {
|
||||
properties: playerProperties,
|
||||
deckList: '',
|
||||
zones: {},
|
||||
counters: {},
|
||||
arrows: {},
|
||||
};
|
||||
return updateGame(state, gameId, {
|
||||
players: { ...game.players, [playerProperties.playerId]: newPlayer },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
case Types.PLAYER_LEFT: {
|
||||
const { gameId, playerId } = action;
|
||||
playerLeft: (state, action: PayloadAction<{ gameId: number; playerId: number }>) => {
|
||||
const { gameId, playerId } = action.payload;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
if (game) {
|
||||
delete game.players[playerId];
|
||||
}
|
||||
const players = { ...game.players };
|
||||
delete players[playerId];
|
||||
return updateGame(state, gameId, { players });
|
||||
}
|
||||
},
|
||||
|
||||
case Types.PLAYER_PROPERTIES_CHANGED: {
|
||||
return updatePlayer(state, action.gameId, action.playerId, {
|
||||
properties: action.properties,
|
||||
});
|
||||
}
|
||||
playerPropertiesChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ gameId: number; playerId: number; properties: Data.ServerInfo_PlayerProperties }>,
|
||||
) => {
|
||||
const { gameId, playerId, properties } = action.payload;
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (player) {
|
||||
player.properties = properties;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Card manipulation ────────────────────────────────────────────────────
|
||||
|
||||
case Types.CARD_MOVED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardMoved: (
|
||||
state,
|
||||
action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_MoveCard }>,
|
||||
) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const {
|
||||
cardId,
|
||||
cardName,
|
||||
startPlayerId,
|
||||
startZone,
|
||||
position,
|
||||
targetPlayerId,
|
||||
targetZone,
|
||||
x,
|
||||
y,
|
||||
newCardId,
|
||||
faceDown,
|
||||
newCardProviderId,
|
||||
cardId, cardName, startPlayerId, startZone, position,
|
||||
targetPlayerId, targetZone, x, y, newCardId, faceDown, newCardProviderId,
|
||||
} = data;
|
||||
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStartPlayerId = startPlayerId >= 0 ? startPlayerId : playerId;
|
||||
const sourcePlayer = game.players[effectiveStartPlayerId];
|
||||
const sourceZoneEntry = sourcePlayer?.zones[startZone];
|
||||
if (!sourcePlayer || !sourceZoneEntry) {
|
||||
return state;
|
||||
const sourceZone = sourcePlayer?.zones[startZone];
|
||||
if (!sourcePlayer || !sourceZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Locate card in source zone (by id for visible zones, by position for hidden)
|
||||
let removedCard: Data.ServerInfo_Card | undefined;
|
||||
let newSourceCards: Data.ServerInfo_Card[];
|
||||
let resolvedCardId = -1;
|
||||
if (cardId >= 0) {
|
||||
removedCard = sourceZoneEntry.cards.find(c => c.id === cardId);
|
||||
newSourceCards = sourceZoneEntry.cards.filter(c => c.id !== cardId);
|
||||
} else if (position >= 0 && position < sourceZoneEntry.cards.length) {
|
||||
removedCard = sourceZoneEntry.cards[position];
|
||||
newSourceCards = sourceZoneEntry.cards.filter((_, i) => i !== position);
|
||||
} else {
|
||||
// Hidden zone with unknown position — just decrement count
|
||||
newSourceCards = sourceZoneEntry.cards;
|
||||
resolvedCardId = cardId;
|
||||
} else if (position >= 0 && position < sourceZone.order.length) {
|
||||
resolvedCardId = sourceZone.order[position];
|
||||
}
|
||||
|
||||
const removedCard: Data.ServerInfo_Card | undefined =
|
||||
resolvedCardId >= 0 ? sourceZone.byId[resolvedCardId] : undefined;
|
||||
|
||||
if (resolvedCardId >= 0) {
|
||||
const idx = sourceZone.order.indexOf(resolvedCardId);
|
||||
if (idx >= 0) {
|
||||
sourceZone.order.splice(idx, 1);
|
||||
}
|
||||
delete sourceZone.byId[resolvedCardId];
|
||||
}
|
||||
sourceZone.cardCount = Math.max(0, sourceZone.cardCount - 1);
|
||||
|
||||
const effectiveNewId = newCardId >= 0 ? newCardId : (removedCard?.id ?? -1);
|
||||
const movedCard: Data.ServerInfo_Card = removedCard
|
||||
? {
|
||||
...removedCard,
|
||||
id: effectiveNewId,
|
||||
name: cardName || removedCard.name,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
providerId: newCardProviderId || removedCard.providerId,
|
||||
...removedCard, id: effectiveNewId, name: cardName || removedCard.name,
|
||||
x, y, faceDown, providerId: newCardProviderId || removedCard.providerId,
|
||||
}
|
||||
: buildEmptyCard(effectiveNewId, cardName, x, y, faceDown, newCardProviderId ?? '');
|
||||
|
||||
let newState = updateZone(state, gameId, effectiveStartPlayerId, startZone, {
|
||||
cards: newSourceCards,
|
||||
cardCount: Math.max(0, sourceZoneEntry.cardCount - 1),
|
||||
});
|
||||
|
||||
const updatedGame = newState.games[gameId];
|
||||
const targetPlayer = updatedGame?.players[targetPlayerId];
|
||||
const targetPlayer = game.players[targetPlayerId];
|
||||
const targetZoneEntry = targetPlayer?.zones[targetZone];
|
||||
if (!targetPlayer || !targetZoneEntry) {
|
||||
return newState;
|
||||
return;
|
||||
}
|
||||
|
||||
newState = updateZone(newState, gameId, targetPlayerId, targetZone, {
|
||||
cards: [...targetZoneEntry.cards, movedCard],
|
||||
cardCount: targetZoneEntry.cardCount + 1,
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
targetZoneEntry.order.push(movedCard.id);
|
||||
targetZoneEntry.byId[movedCard.id] = movedCard;
|
||||
targetZoneEntry.cardCount++;
|
||||
},
|
||||
|
||||
case Types.CARD_FLIPPED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardFlipped: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_FlipCard }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cardId, cardName, faceDown, cardProviderId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
const card = state.games[gameId]?.players[playerId]?.zones[zoneName]?.byId[cardId];
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
card.faceDown = faceDown;
|
||||
if (cardName) {
|
||||
card.name = cardName;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
if (cardProviderId) {
|
||||
card.providerId = cardProviderId;
|
||||
}
|
||||
},
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = {
|
||||
...updatedCards[cardIdx],
|
||||
faceDown,
|
||||
name: cardName || updatedCards[cardIdx].name,
|
||||
providerId: cardProviderId || updatedCards[cardIdx].providerId,
|
||||
};
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.CARD_DESTROYED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardDestroyed: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_DestroyCard }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cardId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
const zone = state.games[gameId]?.players[playerId]?.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
const idx = zone.order.indexOf(cardId);
|
||||
if (idx >= 0) {
|
||||
zone.order.splice(idx, 1);
|
||||
}
|
||||
delete zone.byId[cardId];
|
||||
zone.cardCount = Math.max(0, zone.cardCount - 1);
|
||||
},
|
||||
|
||||
return updateZone(state, gameId, playerId, zoneName, {
|
||||
cards: zone.cards.filter(c => c.id !== cardId),
|
||||
cardCount: Math.max(0, zone.cardCount - 1),
|
||||
});
|
||||
}
|
||||
|
||||
case Types.CARD_ATTACHED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardAttached: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_AttachCard }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { startZone, cardId, targetPlayerId, targetZone, targetCardId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
const card = state.games[gameId]?.players[playerId]?.zones[startZone]?.byId[cardId];
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[startZone];
|
||||
card.attachPlayerId = targetPlayerId;
|
||||
card.attachZone = targetZone;
|
||||
card.attachCardId = targetCardId;
|
||||
},
|
||||
|
||||
tokenCreated: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_CreateToken }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cardId, cardName, color, pt, annotation, destroyOnZoneChange, x, y, cardProviderId, faceDown } = data;
|
||||
const zone = state.games[gameId]?.players[playerId]?.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = {
|
||||
...updatedCards[cardIdx],
|
||||
attachPlayerId: targetPlayerId,
|
||||
attachZone: targetZone,
|
||||
attachCardId: targetCardId,
|
||||
};
|
||||
return updateZone(state, gameId, playerId, startZone, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.TOKEN_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const {
|
||||
zoneName,
|
||||
cardId,
|
||||
cardName,
|
||||
color,
|
||||
pt,
|
||||
annotation,
|
||||
destroyOnZoneChange,
|
||||
x,
|
||||
y,
|
||||
cardProviderId,
|
||||
faceDown,
|
||||
} = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newCard: Data.ServerInfo_Card = create(Data.ServerInfo_CardSchema, {
|
||||
id: cardId,
|
||||
name: cardName,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
tapped: false,
|
||||
attacking: false,
|
||||
color,
|
||||
pt,
|
||||
annotation,
|
||||
destroyOnZoneChange,
|
||||
doesntUntap: false,
|
||||
counterList: [],
|
||||
attachPlayerId: -1,
|
||||
attachZone: '',
|
||||
attachCardId: -1,
|
||||
providerId: cardProviderId,
|
||||
const newCard = create(Data.ServerInfo_CardSchema, {
|
||||
id: cardId, name: cardName, x, y, faceDown,
|
||||
tapped: false, attacking: false, color, pt, annotation, destroyOnZoneChange,
|
||||
doesntUntap: false, counterList: [],
|
||||
attachPlayerId: -1, attachZone: '', attachCardId: -1, providerId: cardProviderId,
|
||||
});
|
||||
return updateZone(state, gameId, playerId, zoneName, {
|
||||
cards: [...zone.cards, newCard],
|
||||
cardCount: zone.cardCount + 1,
|
||||
});
|
||||
}
|
||||
zone.order.push(newCard.id);
|
||||
zone.byId[newCard.id] = newCard;
|
||||
zone.cardCount++;
|
||||
},
|
||||
|
||||
case Types.CARD_ATTR_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardAttrChanged: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_SetCardAttr }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cardId, attribute, attrValue } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
const card = state.games[gameId]?.players[playerId]?.zones[zoneName]?.byId[cardId];
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const attrPatch: Partial<Data.ServerInfo_Card> = {};
|
||||
switch (attribute as Data.CardAttribute) {
|
||||
case Data.CardAttribute.AttrTapped: attrPatch.tapped = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrAttacking: attrPatch.attacking = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrFaceDown: attrPatch.faceDown = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrColor: attrPatch.color = attrValue; break;
|
||||
case Data.CardAttribute.AttrPT: attrPatch.pt = attrValue; break;
|
||||
case Data.CardAttribute.AttrAnnotation: attrPatch.annotation = attrValue; break;
|
||||
case Data.CardAttribute.AttrDoesntUntap: attrPatch.doesntUntap = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrTapped: card.tapped = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrAttacking: card.attacking = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrFaceDown: card.faceDown = attrValue === '1'; break;
|
||||
case Data.CardAttribute.AttrColor: card.color = attrValue; break;
|
||||
case Data.CardAttribute.AttrPT: card.pt = attrValue; break;
|
||||
case Data.CardAttribute.AttrAnnotation: card.annotation = attrValue; break;
|
||||
case Data.CardAttribute.AttrDoesntUntap: card.doesntUntap = attrValue === '1'; break;
|
||||
}
|
||||
},
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = { ...updatedCards[cardIdx], ...attrPatch };
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.CARD_COUNTER_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardCounterChanged: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_SetCardCounter }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cardId, counterId, counterValue } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
const card = state.games[gameId]?.players[playerId]?.zones[zoneName]?.byId[cardId];
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const card = zone.cards[cardIdx];
|
||||
let newCounterList: Data.ServerInfo_CardCounter[];
|
||||
if (counterValue <= 0) {
|
||||
newCounterList = card.counterList.filter(c => c.id !== counterId);
|
||||
card.counterList = card.counterList.filter(c => c.id !== counterId);
|
||||
} else {
|
||||
const existing = card.counterList.findIndex(c => c.id === counterId);
|
||||
newCounterList =
|
||||
existing >= 0
|
||||
? card.counterList.map(c => (c.id === counterId ? { ...c, value: counterValue } : c))
|
||||
: [...card.counterList, create(Data.ServerInfo_CardCounterSchema, { id: counterId, value: counterValue })];
|
||||
const idx = card.counterList.findIndex(c => c.id === counterId);
|
||||
if (idx >= 0) {
|
||||
card.counterList[idx] = { ...card.counterList[idx], value: counterValue };
|
||||
} else {
|
||||
card.counterList.push(create(Data.ServerInfo_CardCounterSchema, { id: counterId, value: counterValue }));
|
||||
}
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = { ...card, counterList: newCounterList };
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Arrows ───────────────────────────────────────────────────────────────
|
||||
|
||||
case Types.ARROW_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { arrowInfo } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
arrowCreated: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_CreateArrow }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (player) {
|
||||
player.arrows[data.arrowInfo.id] = data.arrowInfo;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
arrows: { ...player.arrows, [arrowInfo.id]: arrowInfo },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
case Types.ARROW_DELETED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { arrowId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
arrowDeleted: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_DeleteArrow }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (player) {
|
||||
delete player.arrows[data.arrowId];
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const arrows = { ...player.arrows };
|
||||
delete arrows[arrowId];
|
||||
return updatePlayer(state, gameId, playerId, { arrows });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Player counters ───────────────────────────────────────────────────────
|
||||
|
||||
case Types.COUNTER_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterInfo } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
counterCreated: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_CreateCounter }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (player) {
|
||||
player.counters[data.counterInfo.id] = data.counterInfo;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
counters: { ...player.counters, [counterInfo.id]: counterInfo },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
case Types.COUNTER_SET: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterId, value } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
counterSet: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_SetCounter }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const counter = state.games[gameId]?.players[playerId]?.counters[data.counterId];
|
||||
if (counter) {
|
||||
counter.count = data.value;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const counter = player.counters[counterId];
|
||||
if (!counter) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
counters: { ...player.counters, [counterId]: { ...counter, count: value } },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
case Types.COUNTER_DELETED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
counterDeleted: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_DelCounter }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (player) {
|
||||
delete player.counters[data.counterId];
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const counters = { ...player.counters };
|
||||
delete counters[counterId];
|
||||
return updatePlayer(state, gameId, playerId, { counters });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Zone operations ───────────────────────────────────────────────────────
|
||||
|
||||
case Types.CARDS_DRAWN: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardsDrawn: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_DrawCards }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { number: drawCount, cards } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
const player = state.games[gameId]?.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
const deckZone = player.zones['deck'];
|
||||
const handZone = player.zones['hand'];
|
||||
if (!handZone) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrement deck count for the drawing player
|
||||
let newState = deckZone
|
||||
? updateZone(state, gameId, playerId, 'deck', {
|
||||
cardCount: Math.max(0, deckZone.cardCount - drawCount),
|
||||
})
|
||||
: state;
|
||||
if (deckZone) {
|
||||
deckZone.cardCount = Math.max(0, deckZone.cardCount - drawCount);
|
||||
}
|
||||
|
||||
// Append revealed cards to hand (cards array is empty for non-drawing players;
|
||||
// use drawCount for count math so all observers track the correct hand/deck size)
|
||||
const updatedHand = newState.games[gameId]!.players[playerId]!.zones['hand']!;
|
||||
return updateZone(newState, gameId, playerId, 'hand', {
|
||||
cards: [...updatedHand.cards, ...cards],
|
||||
cardCount: updatedHand.cardCount + drawCount,
|
||||
});
|
||||
}
|
||||
for (const card of cards) {
|
||||
handZone.order.push(card.id);
|
||||
handZone.byId[card.id] = card;
|
||||
}
|
||||
handZone.cardCount += drawCount;
|
||||
},
|
||||
|
||||
case Types.CARDS_REVEALED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
cardsRevealed: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_RevealCards }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const { zoneName, cards } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
const zone = state.games[gameId]?.players[playerId]?.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge revealed card data into existing zone cards (update existing, append new)
|
||||
const merged = [...zone.cards];
|
||||
for (const revealedCard of cards) {
|
||||
const idx = merged.findIndex(c => c.id === revealedCard.id);
|
||||
if (idx >= 0) {
|
||||
merged[idx] = { ...merged[idx], ...revealedCard };
|
||||
if (zone.byId[revealedCard.id]) {
|
||||
Object.assign(zone.byId[revealedCard.id], revealedCard);
|
||||
} else {
|
||||
merged.push(revealedCard);
|
||||
zone.order.push(revealedCard.id);
|
||||
zone.byId[revealedCard.id] = revealedCard;
|
||||
}
|
||||
}
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: merged });
|
||||
}
|
||||
},
|
||||
|
||||
case Types.ZONE_PROPERTIES_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, alwaysRevealTopCard, alwaysLookAtTopCard } = data;
|
||||
const patch: Partial<ZoneEntry> = {};
|
||||
if (alwaysRevealTopCard !== undefined && alwaysRevealTopCard !== null) {
|
||||
patch.alwaysRevealTopCard = alwaysRevealTopCard;
|
||||
zonePropertiesChanged: (state, action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_ChangeZoneProperties }>) => {
|
||||
const { gameId, playerId, data } = action.payload;
|
||||
const zone = state.games[gameId]?.players[playerId]?.zones[data.zoneName];
|
||||
if (!zone) {
|
||||
return;
|
||||
}
|
||||
if (alwaysLookAtTopCard !== undefined && alwaysLookAtTopCard !== null) {
|
||||
patch.alwaysLookAtTopCard = alwaysLookAtTopCard;
|
||||
if (data.alwaysRevealTopCard !== undefined && data.alwaysRevealTopCard !== null) {
|
||||
zone.alwaysRevealTopCard = data.alwaysRevealTopCard;
|
||||
}
|
||||
return updateZone(state, gameId, playerId, zoneName, patch);
|
||||
}
|
||||
if (data.alwaysLookAtTopCard !== undefined && data.alwaysLookAtTopCard !== null) {
|
||||
zone.alwaysLookAtTopCard = data.alwaysLookAtTopCard;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Turn / phase ──────────────────────────────────────────────────────────
|
||||
|
||||
case Types.ACTIVE_PLAYER_SET: {
|
||||
return updateGame(state, action.gameId, { activePlayerId: action.activePlayerId });
|
||||
}
|
||||
activePlayerSet: (state, action: PayloadAction<{ gameId: number; activePlayerId: number }>) => {
|
||||
const game = state.games[action.payload.gameId];
|
||||
if (game) {
|
||||
game.activePlayerId = action.payload.activePlayerId;
|
||||
}
|
||||
},
|
||||
|
||||
case Types.ACTIVE_PHASE_SET: {
|
||||
return updateGame(state, action.gameId, { activePhase: action.phase });
|
||||
}
|
||||
activePhaseSet: (state, action: PayloadAction<{ gameId: number; phase: number }>) => {
|
||||
const game = state.games[action.payload.gameId];
|
||||
if (game) {
|
||||
game.activePhase = action.payload.phase;
|
||||
}
|
||||
},
|
||||
|
||||
case Types.TURN_REVERSED: {
|
||||
return updateGame(state, action.gameId, { reversed: action.reversed });
|
||||
}
|
||||
turnReversed: (state, action: PayloadAction<{ gameId: number; reversed: boolean }>) => {
|
||||
const game = state.games[action.payload.gameId];
|
||||
if (game) {
|
||||
game.reversed = action.payload.reversed;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────────────────────────
|
||||
|
||||
case Types.GAME_SAY: {
|
||||
const { gameId, playerId, message } = action;
|
||||
gameSay: (state, action: PayloadAction<{ gameId: number; playerId: number; message: string }>) => {
|
||||
const { gameId, playerId, message } = action.payload;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
const newMessage: GameMessage = { playerId, message, timeReceived: Date.now() };
|
||||
return updateGame(state, gameId, {
|
||||
messages: [...game.messages, newMessage],
|
||||
});
|
||||
}
|
||||
if (game.messages.length >= MAX_GAME_MESSAGES) {
|
||||
game.messages = game.messages.slice(game.messages.length - MAX_GAME_MESSAGES + 1);
|
||||
}
|
||||
game.messages.push({ playerId, message, timeReceived: Date.now() });
|
||||
},
|
||||
|
||||
// ── Log-only events (state unchanged, future game log will use these) ─────
|
||||
case Types.ZONE_SHUFFLED:
|
||||
case Types.ZONE_DUMPED:
|
||||
case Types.DIE_ROLLED: {
|
||||
return state;
|
||||
}
|
||||
// ── Log-only events ─────────────────────────────────────────────────────
|
||||
zoneShuffled: (_state, _action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_Shuffle }>) => {},
|
||||
zoneDumped: (_state, _action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_DumpZone }>) => {},
|
||||
dieRolled: (_state, _action: PayloadAction<{ gameId: number; playerId: number; data: Data.Event_RollDie }>) => {},
|
||||
},
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
export const gamesReducer = gamesSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ describe('Selectors', () => {
|
|||
it('getActiveGameIds → returns numeric array of gameIds', () => {
|
||||
const state = makeState({
|
||||
games: {
|
||||
1: makeGameEntry({ gameId: 1 }),
|
||||
2: makeGameEntry({ gameId: 2 }),
|
||||
1: makeGameEntry(),
|
||||
2: makeGameEntry(),
|
||||
},
|
||||
});
|
||||
const ids = Selectors.getActiveGameIds(rootState(state));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { Data } from '@app/types';
|
||||
import { GamesState, GameEntry, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||
import type { Data, Enriched } from '@app/types';
|
||||
import { GamesState } from './game.interfaces';
|
||||
|
||||
interface State {
|
||||
games: GamesState;
|
||||
|
|
@ -9,21 +9,39 @@ interface State {
|
|||
const EMPTY_ARRAY: Data.ServerInfo_Card[] = [];
|
||||
const EMPTY_OBJECT = {} as Record<string, never>;
|
||||
|
||||
/**
|
||||
* Memoized cache for materialized zone card arrays. Keyed by the zone object
|
||||
* identity so that repeated selector calls on the same zone reuse the same
|
||||
* array reference — this preserves React referential equality and avoids
|
||||
* spurious re-renders when `getCards` is called from a selector.
|
||||
*/
|
||||
const zoneCardsCache = new WeakMap<Enriched.ZoneEntry, Data.ServerInfo_Card[]>();
|
||||
|
||||
function materializeZoneCards(zone: Enriched.ZoneEntry): Data.ServerInfo_Card[] {
|
||||
const cached = zoneCardsCache.get(zone);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const arr = zone.order.map(id => zone.byId[id]);
|
||||
zoneCardsCache.set(zone, arr);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const Selectors = {
|
||||
getGames: ({ games }: State): { [gameId: number]: GameEntry } => games.games,
|
||||
getGames: ({ games }: State): { [gameId: number]: Enriched.GameEntry } => games.games,
|
||||
|
||||
getGame: ({ games }: State, gameId: number): GameEntry | undefined => games.games[gameId],
|
||||
getGame: ({ games }: State, gameId: number): Enriched.GameEntry | undefined => games.games[gameId],
|
||||
|
||||
getPlayers: ({ games }: State, gameId: number): { [playerId: number]: PlayerEntry } | undefined =>
|
||||
getPlayers: ({ games }: State, gameId: number): { [playerId: number]: Enriched.PlayerEntry } | undefined =>
|
||||
games.games[gameId]?.players,
|
||||
|
||||
getPlayer: ({ games }: State, gameId: number, playerId: number): PlayerEntry | undefined =>
|
||||
getPlayer: ({ games }: State, gameId: number, playerId: number): Enriched.PlayerEntry | undefined =>
|
||||
games.games[gameId]?.players[playerId],
|
||||
|
||||
getLocalPlayerId: ({ games }: State, gameId: number): number | undefined =>
|
||||
games.games[gameId]?.localPlayerId,
|
||||
|
||||
getLocalPlayer: (state: State, gameId: number): PlayerEntry | undefined => {
|
||||
getLocalPlayer: (state: State, gameId: number): Enriched.PlayerEntry | undefined => {
|
||||
const game = state.games.games[gameId];
|
||||
if (!game) {
|
||||
return undefined;
|
||||
|
|
@ -35,7 +53,7 @@ export const Selectors = {
|
|||
{ games }: State,
|
||||
gameId: number,
|
||||
playerId: number
|
||||
): { [zoneName: string]: ZoneEntry } | undefined =>
|
||||
): { [zoneName: string]: Enriched.ZoneEntry } | undefined =>
|
||||
games.games[gameId]?.players[playerId]?.zones,
|
||||
|
||||
getZone: (
|
||||
|
|
@ -43,10 +61,12 @@ export const Selectors = {
|
|||
gameId: number,
|
||||
playerId: number,
|
||||
zoneName: string
|
||||
): ZoneEntry | undefined => games.games[gameId]?.players[playerId]?.zones[zoneName],
|
||||
): Enriched.ZoneEntry | undefined => games.games[gameId]?.players[playerId]?.zones[zoneName],
|
||||
|
||||
getCards: ({ games }: State, gameId: number, playerId: number, zoneName: string) =>
|
||||
games.games[gameId]?.players[playerId]?.zones[zoneName]?.cards ?? EMPTY_ARRAY,
|
||||
getCards: ({ games }: State, gameId: number, playerId: number, zoneName: string): Data.ServerInfo_Card[] => {
|
||||
const zone = games.games[gameId]?.players[playerId]?.zones[zoneName];
|
||||
return zone ? materializeZoneCards(zone) : EMPTY_ARRAY;
|
||||
},
|
||||
|
||||
getCounters: ({ games }: State, gameId: number, playerId: number) =>
|
||||
games.games[gameId]?.players[playerId]?.counters ?? EMPTY_OBJECT,
|
||||
|
|
|
|||
|
|
@ -1,34 +1,40 @@
|
|||
import { gamesSlice } from './game.reducer';
|
||||
|
||||
const a = gamesSlice.actions;
|
||||
|
||||
export const Types = {
|
||||
CLEAR_STORE: '[Games] Clear Store',
|
||||
GAME_JOINED: '[Games] Game Joined',
|
||||
GAME_LEFT: '[Games] Game Left',
|
||||
GAME_CLOSED: '[Games] Game Closed',
|
||||
GAME_HOST_CHANGED: '[Games] Game Host Changed',
|
||||
GAME_STATE_CHANGED: '[Games] Game State Changed',
|
||||
PLAYER_JOINED: '[Games] Player Joined',
|
||||
PLAYER_LEFT: '[Games] Player Left',
|
||||
PLAYER_PROPERTIES_CHANGED: '[Games] Player Properties Changed',
|
||||
KICKED: '[Games] Kicked',
|
||||
CARD_MOVED: '[Games] Card Moved',
|
||||
CARD_FLIPPED: '[Games] Card Flipped',
|
||||
CARD_DESTROYED: '[Games] Card Destroyed',
|
||||
CARD_ATTACHED: '[Games] Card Attached',
|
||||
TOKEN_CREATED: '[Games] Token Created',
|
||||
CARD_ATTR_CHANGED: '[Games] Card Attribute Changed',
|
||||
CARD_COUNTER_CHANGED: '[Games] Card Counter Changed',
|
||||
ARROW_CREATED: '[Games] Arrow Created',
|
||||
ARROW_DELETED: '[Games] Arrow Deleted',
|
||||
COUNTER_CREATED: '[Games] Counter Created',
|
||||
COUNTER_SET: '[Games] Counter Set',
|
||||
COUNTER_DELETED: '[Games] Counter Deleted',
|
||||
CARDS_DRAWN: '[Games] Cards Drawn',
|
||||
CARDS_REVEALED: '[Games] Cards Revealed',
|
||||
ZONE_SHUFFLED: '[Games] Zone Shuffled',
|
||||
DIE_ROLLED: '[Games] Die Rolled',
|
||||
ACTIVE_PLAYER_SET: '[Games] Active Player Set',
|
||||
ACTIVE_PHASE_SET: '[Games] Active Phase Set',
|
||||
TURN_REVERSED: '[Games] Turn Reversed',
|
||||
ZONE_DUMPED: '[Games] Zone Dumped',
|
||||
ZONE_PROPERTIES_CHANGED: '[Games] Zone Properties Changed',
|
||||
GAME_SAY: '[Games] Game Say',
|
||||
CLEAR_STORE: a.clearStore.type,
|
||||
GAME_JOINED: a.gameJoined.type,
|
||||
GAME_LEFT: a.gameLeft.type,
|
||||
GAME_CLOSED: a.gameClosed.type,
|
||||
GAME_HOST_CHANGED: a.gameHostChanged.type,
|
||||
GAME_STATE_CHANGED: a.gameStateChanged.type,
|
||||
PLAYER_JOINED: a.playerJoined.type,
|
||||
PLAYER_LEFT: a.playerLeft.type,
|
||||
PLAYER_PROPERTIES_CHANGED: a.playerPropertiesChanged.type,
|
||||
KICKED: a.kicked.type,
|
||||
CARD_MOVED: a.cardMoved.type,
|
||||
CARD_FLIPPED: a.cardFlipped.type,
|
||||
CARD_DESTROYED: a.cardDestroyed.type,
|
||||
CARD_ATTACHED: a.cardAttached.type,
|
||||
TOKEN_CREATED: a.tokenCreated.type,
|
||||
CARD_ATTR_CHANGED: a.cardAttrChanged.type,
|
||||
CARD_COUNTER_CHANGED: a.cardCounterChanged.type,
|
||||
ARROW_CREATED: a.arrowCreated.type,
|
||||
ARROW_DELETED: a.arrowDeleted.type,
|
||||
COUNTER_CREATED: a.counterCreated.type,
|
||||
COUNTER_SET: a.counterSet.type,
|
||||
COUNTER_DELETED: a.counterDeleted.type,
|
||||
CARDS_DRAWN: a.cardsDrawn.type,
|
||||
CARDS_REVEALED: a.cardsRevealed.type,
|
||||
ZONE_SHUFFLED: a.zoneShuffled.type,
|
||||
DIE_ROLLED: a.dieRolled.type,
|
||||
ACTIVE_PLAYER_SET: a.activePlayerSet.type,
|
||||
ACTIVE_PHASE_SET: a.activePhaseSet.type,
|
||||
TURN_REVERSED: a.turnReversed.type,
|
||||
ZONE_DUMPED: a.zoneDumped.type,
|
||||
ZONE_PROPERTIES_CHANGED: a.zonePropertiesChanged.type,
|
||||
GAME_SAY: a.gameSay.type,
|
||||
} as const;
|
||||
|
||||
export { MAX_GAME_MESSAGES } from './game.reducer';
|
||||
|
|
|
|||
|
|
@ -25,5 +25,3 @@ export {
|
|||
Dispatch as RoomsDispatch } from './rooms';
|
||||
|
||||
export * from './rooms/rooms.interfaces';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,49 @@ export function makeUser(
|
|||
});
|
||||
}
|
||||
|
||||
export function makeRoom(overrides: Partial<Omit<Enriched.Room, '$typeName' | '$unknown'>> = {}): Enriched.Room {
|
||||
const { gametypeMap = {}, order = 0, gameList = [], ...protoOverrides } = overrides;
|
||||
type MakeGameOverrides = MessageInitShape<typeof Data.ServerInfo_GameSchema> & {
|
||||
gameType?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test fixture for Enriched.Game.
|
||||
*
|
||||
* Accepts proto field shorthands (gameId, description, etc.) which populate
|
||||
* `info`, plus the top-level client field `gameType`.
|
||||
*/
|
||||
export function makeGame(overrides: MakeGameOverrides = {}): Enriched.Game {
|
||||
const { gameType = '', ...protoFields } = overrides;
|
||||
return {
|
||||
...create(Data.ServerInfo_RoomSchema, {
|
||||
info: create(Data.ServerInfo_GameSchema, {
|
||||
gameId: 1,
|
||||
roomId: 1,
|
||||
description: 'Test Game',
|
||||
gameTypes: [],
|
||||
started: false,
|
||||
...protoFields,
|
||||
}),
|
||||
gameType,
|
||||
};
|
||||
}
|
||||
|
||||
type MakeRoomOverrides = MessageInitShape<typeof Data.ServerInfo_RoomSchema> & {
|
||||
gametypeMap?: Enriched.GametypeMap;
|
||||
order?: number;
|
||||
games?: { [gameId: number]: Enriched.Game };
|
||||
users?: { [userName: string]: Data.ServerInfo_User };
|
||||
};
|
||||
|
||||
/**
|
||||
* Test fixture for Enriched.Room.
|
||||
*
|
||||
* Accepts proto field shorthands (roomId, name, etc.) which populate `info`,
|
||||
* plus normalized collections (games, users, gametypeMap) and the client-only
|
||||
* `order` field.
|
||||
*/
|
||||
export function makeRoom(overrides: MakeRoomOverrides = {}): Enriched.Room {
|
||||
const { gametypeMap = {}, order = 0, games = {}, users = {}, ...protoFields } = overrides;
|
||||
return {
|
||||
info: create(Data.ServerInfo_RoomSchema, {
|
||||
roomId: 1,
|
||||
name: 'Test Room',
|
||||
description: '',
|
||||
|
|
@ -29,29 +68,12 @@ export function makeRoom(overrides: Partial<Omit<Enriched.Room, '$typeName' | '$
|
|||
autoJoin: false,
|
||||
playerCount: 0,
|
||||
userList: [],
|
||||
...protoOverrides,
|
||||
...protoFields,
|
||||
}),
|
||||
gameList,
|
||||
gametypeMap,
|
||||
order,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeGame(
|
||||
overrides: Partial<Omit<Enriched.Game & { startTime: number }, '$typeName' | '$unknown'>> = {},
|
||||
): Enriched.Game & { startTime: number } {
|
||||
const { gameType = '', startTime = 0, ...protoOverrides } = overrides;
|
||||
return {
|
||||
...create(Data.ServerInfo_GameSchema, {
|
||||
gameId: 1,
|
||||
roomId: 1,
|
||||
description: 'Test Game',
|
||||
gameTypes: [],
|
||||
started: false,
|
||||
...protoOverrides,
|
||||
}),
|
||||
gameType,
|
||||
startTime,
|
||||
games,
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +94,6 @@ export function makeRoomsState(overrides: Partial<RoomsState> = {}): RoomsState
|
|||
rooms: {
|
||||
1: makeRoom({ roomId: 1 }),
|
||||
},
|
||||
games: {},
|
||||
joinedRoomIds: {},
|
||||
joinedGameIds: {},
|
||||
messages: {},
|
||||
|
|
|
|||
|
|
@ -5,65 +5,68 @@ import { App } from '@app/types';
|
|||
|
||||
describe('Actions', () => {
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE, payload: undefined });
|
||||
});
|
||||
|
||||
it('updateRooms', () => {
|
||||
const rooms = [makeRoom()];
|
||||
expect(Actions.updateRooms(rooms)).toEqual({ type: Types.UPDATE_ROOMS, rooms });
|
||||
expect(Actions.updateRooms({ rooms })).toEqual({ type: Types.UPDATE_ROOMS, payload: { rooms } });
|
||||
});
|
||||
|
||||
it('joinRoom', () => {
|
||||
const roomInfo = makeRoom({ roomId: 2 });
|
||||
expect(Actions.joinRoom(roomInfo)).toEqual({ type: Types.JOIN_ROOM, roomInfo });
|
||||
expect(Actions.joinRoom({ roomInfo })).toEqual({ type: Types.JOIN_ROOM, payload: { roomInfo } });
|
||||
});
|
||||
|
||||
it('leaveRoom', () => {
|
||||
expect(Actions.leaveRoom(3)).toEqual({ type: Types.LEAVE_ROOM, roomId: 3 });
|
||||
expect(Actions.leaveRoom({ roomId: 3 })).toEqual({ type: Types.LEAVE_ROOM, payload: { roomId: 3 } });
|
||||
});
|
||||
|
||||
it('addMessage', () => {
|
||||
const message = makeMessage();
|
||||
expect(Actions.addMessage(1, message)).toEqual({ type: Types.ADD_MESSAGE, roomId: 1, message });
|
||||
expect(Actions.addMessage({ roomId: 1, message })).toEqual({ type: Types.ADD_MESSAGE, payload: { roomId: 1, message } });
|
||||
});
|
||||
|
||||
it('updateGames', () => {
|
||||
const games = [makeGame()];
|
||||
expect(Actions.updateGames(1, games)).toEqual({ type: Types.UPDATE_GAMES, roomId: 1, games });
|
||||
expect(Actions.updateGames({ roomId: 1, games })).toEqual({ type: Types.UPDATE_GAMES, payload: { roomId: 1, games } });
|
||||
});
|
||||
|
||||
it('userJoined', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.userJoined(1, user)).toEqual({ type: Types.USER_JOINED, roomId: 1, user });
|
||||
expect(Actions.userJoined({ roomId: 1, user })).toEqual({ type: Types.USER_JOINED, payload: { roomId: 1, user } });
|
||||
});
|
||||
|
||||
it('userLeft', () => {
|
||||
expect(Actions.userLeft(1, 'Alice')).toEqual({ type: Types.USER_LEFT, roomId: 1, name: 'Alice' });
|
||||
expect(Actions.userLeft({ roomId: 1, name: 'Alice' })).toEqual({ type: Types.USER_LEFT, payload: { roomId: 1, name: 'Alice' } });
|
||||
});
|
||||
|
||||
it('sortGames', () => {
|
||||
expect(Actions.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC)).toEqual({
|
||||
expect(Actions.sortGames({ field: App.GameSortField.START_TIME, order: App.SortDirection.ASC })).toEqual({
|
||||
type: Types.SORT_GAMES,
|
||||
roomId: 1,
|
||||
field: App.GameSortField.START_TIME,
|
||||
order: App.SortDirection.ASC,
|
||||
payload: {
|
||||
field: App.GameSortField.START_TIME,
|
||||
order: App.SortDirection.ASC,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removeMessages', () => {
|
||||
expect(Actions.removeMessages(1, 'Alice', 3)).toEqual({
|
||||
expect(Actions.removeMessages({ roomId: 1, name: 'Alice', amount: 3 })).toEqual({
|
||||
type: Types.REMOVE_MESSAGES,
|
||||
roomId: 1,
|
||||
name: 'Alice',
|
||||
amount: 3,
|
||||
payload: {
|
||||
roomId: 1,
|
||||
name: 'Alice',
|
||||
amount: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('gameCreated', () => {
|
||||
expect(Actions.gameCreated(2)).toEqual({ type: Types.GAME_CREATED, roomId: 2 });
|
||||
expect(Actions.gameCreated({ roomId: 2 })).toEqual({ type: Types.GAME_CREATED, payload: { roomId: 2 } });
|
||||
});
|
||||
|
||||
it('joinedGame', () => {
|
||||
expect(Actions.joinedGame(1, 5)).toEqual({ type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
|
||||
expect(Actions.joinedGame({ roomId: 1, gameId: 5 })).toEqual({ type: Types.JOINED_GAME, payload: { roomId: 1, gameId: 5 } });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +1,5 @@
|
|||
import { App, Data, Enriched } from '@app/types';
|
||||
import { roomsSlice } from './rooms.reducer';
|
||||
|
||||
import { Types } from './rooms.types';
|
||||
|
||||
export const Actions = {
|
||||
clearStore: () => ({
|
||||
type: Types.CLEAR_STORE,
|
||||
}),
|
||||
|
||||
updateRooms: (rooms: Data.ServerInfo_Room[]) => ({
|
||||
type: Types.UPDATE_ROOMS,
|
||||
rooms,
|
||||
}),
|
||||
|
||||
joinRoom: (roomInfo: Data.ServerInfo_Room) => ({
|
||||
type: Types.JOIN_ROOM,
|
||||
roomInfo,
|
||||
}),
|
||||
|
||||
leaveRoom: (roomId: number) => ({
|
||||
type: Types.LEAVE_ROOM,
|
||||
roomId,
|
||||
}),
|
||||
|
||||
addMessage: (roomId: number, message: Enriched.Message) => ({
|
||||
type: Types.ADD_MESSAGE,
|
||||
roomId,
|
||||
message,
|
||||
}),
|
||||
|
||||
updateGames: (roomId: number, games: Data.ServerInfo_Game[]) => ({
|
||||
type: Types.UPDATE_GAMES,
|
||||
roomId,
|
||||
games,
|
||||
}),
|
||||
|
||||
userJoined: (roomId: number, user: Data.ServerInfo_User) => ({
|
||||
type: Types.USER_JOINED,
|
||||
roomId,
|
||||
user,
|
||||
}),
|
||||
|
||||
userLeft: (roomId: number, name: string) => ({
|
||||
type: Types.USER_LEFT,
|
||||
roomId,
|
||||
name,
|
||||
}),
|
||||
|
||||
sortGames: (roomId: number, field: App.GameSortField, order: App.SortDirection) => ({
|
||||
type: Types.SORT_GAMES,
|
||||
roomId,
|
||||
field,
|
||||
order,
|
||||
}),
|
||||
|
||||
removeMessages: (roomId: number, name: string, amount: number) => ({
|
||||
type: Types.REMOVE_MESSAGES,
|
||||
roomId,
|
||||
name,
|
||||
amount,
|
||||
}),
|
||||
|
||||
gameCreated: (roomId: number) => ({
|
||||
type: Types.GAME_CREATED,
|
||||
roomId,
|
||||
}),
|
||||
|
||||
joinedGame: (roomId: number, gameId: number) => ({
|
||||
type: Types.JOINED_GAME,
|
||||
roomId,
|
||||
gameId,
|
||||
}),
|
||||
}
|
||||
export const Actions = roomsSlice.actions;
|
||||
|
||||
export type RoomsAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||
|
|
|
|||
|
|
@ -26,70 +26,70 @@ describe('Dispatch', () => {
|
|||
it('updateRooms dispatches Actions.updateRooms()', () => {
|
||||
const rooms = [makeRoom()];
|
||||
Dispatch.updateRooms(rooms);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateRooms(rooms));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateRooms({ rooms }));
|
||||
});
|
||||
|
||||
it('joinRoom dispatches Actions.joinRoom()', () => {
|
||||
const roomInfo = makeRoom({ roomId: 2 });
|
||||
Dispatch.joinRoom(roomInfo);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.joinRoom(roomInfo));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.joinRoom({ roomInfo }));
|
||||
});
|
||||
|
||||
it('leaveRoom dispatches Actions.leaveRoom()', () => {
|
||||
Dispatch.leaveRoom(3);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.leaveRoom(3));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.leaveRoom({ roomId: 3 }));
|
||||
});
|
||||
|
||||
it('addMessage with message.name falsy → dispatches only Actions.addMessage()', () => {
|
||||
const message = { ...makeMessage(), name: undefined };
|
||||
Dispatch.addMessage(1, message);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addMessage({ roomId: 1, message }));
|
||||
});
|
||||
|
||||
it('addMessage with message.name truthy → dispatches Actions.addMessage()', () => {
|
||||
const message = { ...makeMessage(), name: 'Alice' };
|
||||
Dispatch.addMessage(1, message);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addMessage({ roomId: 1, message }));
|
||||
});
|
||||
|
||||
it('updateGames dispatches Actions.updateGames()', () => {
|
||||
const games = [makeGame()];
|
||||
Dispatch.updateGames(1, games);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateGames(1, games));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateGames({ roomId: 1, games }));
|
||||
});
|
||||
|
||||
it('userJoined dispatches Actions.userJoined()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.userJoined(1, user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userJoined(1, user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userJoined({ roomId: 1, user }));
|
||||
});
|
||||
|
||||
it('userLeft dispatches Actions.userLeft()', () => {
|
||||
Dispatch.userLeft(1, 'Alice');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userLeft(1, 'Alice'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userLeft({ roomId: 1, name: 'Alice' }));
|
||||
});
|
||||
|
||||
it('sortGames dispatches Actions.sortGames()', () => {
|
||||
Dispatch.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
Actions.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC)
|
||||
Actions.sortGames({ field: App.GameSortField.START_TIME, order: App.SortDirection.ASC })
|
||||
);
|
||||
});
|
||||
|
||||
it('removeMessages dispatches Actions.removeMessages()', () => {
|
||||
Dispatch.removeMessages(1, 'Alice', 5);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeMessages(1, 'Alice', 5));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeMessages({ roomId: 1, name: 'Alice', amount: 5 }));
|
||||
});
|
||||
|
||||
it('gameCreated dispatches Actions.gameCreated()', () => {
|
||||
Dispatch.gameCreated(2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameCreated(2));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gameCreated({ roomId: 2 }));
|
||||
});
|
||||
|
||||
it('joinedGame dispatches Actions.joinedGame()', () => {
|
||||
Dispatch.joinedGame(1, 5);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.joinedGame(1, 5));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.joinedGame({ roomId: 1, gameId: 5 }));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,47 +9,46 @@ export const Dispatch = {
|
|||
},
|
||||
|
||||
updateRooms: (rooms: Data.ServerInfo_Room[]) => {
|
||||
store.dispatch(Actions.updateRooms(rooms));
|
||||
store.dispatch(Actions.updateRooms({ rooms }));
|
||||
},
|
||||
|
||||
joinRoom: (roomInfo: Data.ServerInfo_Room) => {
|
||||
store.dispatch(Actions.joinRoom(roomInfo));
|
||||
|
||||
store.dispatch(Actions.joinRoom({ roomInfo }));
|
||||
},
|
||||
|
||||
leaveRoom: (roomId: number) => {
|
||||
store.dispatch(Actions.leaveRoom(roomId));
|
||||
store.dispatch(Actions.leaveRoom({ roomId }));
|
||||
},
|
||||
|
||||
addMessage: (roomId: number, message: Enriched.Message) => {
|
||||
store.dispatch(Actions.addMessage(roomId, message));
|
||||
store.dispatch(Actions.addMessage({ roomId, message }));
|
||||
},
|
||||
|
||||
updateGames: (roomId: number, games: Data.ServerInfo_Game[]) => {
|
||||
store.dispatch(Actions.updateGames(roomId, games));
|
||||
store.dispatch(Actions.updateGames({ roomId, games }));
|
||||
},
|
||||
|
||||
userJoined: (roomId: number, user: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.userJoined(roomId, user));
|
||||
store.dispatch(Actions.userJoined({ roomId, user }));
|
||||
},
|
||||
|
||||
userLeft: (roomId: number, name: string) => {
|
||||
store.dispatch(Actions.userLeft(roomId, name));
|
||||
store.dispatch(Actions.userLeft({ roomId, name }));
|
||||
},
|
||||
|
||||
sortGames: (roomId: number, field: App.GameSortField, order: App.SortDirection) => {
|
||||
store.dispatch(Actions.sortGames(roomId, field, order));
|
||||
store.dispatch(Actions.sortGames({ field, order }));
|
||||
},
|
||||
|
||||
removeMessages: (roomId: number, name: string, amount: number) => {
|
||||
store.dispatch(Actions.removeMessages(roomId, name, amount));
|
||||
store.dispatch(Actions.removeMessages({ roomId, name, amount }));
|
||||
},
|
||||
|
||||
gameCreated: (roomId: number) => {
|
||||
store.dispatch(Actions.gameCreated(roomId));
|
||||
store.dispatch(Actions.gameCreated({ roomId }));
|
||||
},
|
||||
|
||||
joinedGame: (roomId: number, gameId: number) => {
|
||||
store.dispatch(Actions.joinedGame(roomId, gameId));
|
||||
store.dispatch(Actions.joinedGame({ roomId, gameId }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { App, Enriched } from '@app/types';
|
|||
|
||||
export interface RoomsState {
|
||||
rooms: RoomsStateRooms;
|
||||
games: RoomsStateGames;
|
||||
joinedRoomIds: JoinedRooms;
|
||||
joinedGameIds: JoinedGames;
|
||||
messages: RoomsStateMessages;
|
||||
|
|
@ -14,12 +13,6 @@ export interface RoomsStateRooms {
|
|||
[roomId: number]: Enriched.Room;
|
||||
}
|
||||
|
||||
export interface RoomsStateGames {
|
||||
[roomId: number]: {
|
||||
[gameId: number]: Enriched.Game;
|
||||
};
|
||||
}
|
||||
|
||||
export interface JoinedRooms {
|
||||
[roomId: number]: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App } from '@app/types';
|
||||
import { roomsReducer } from './rooms.reducer';
|
||||
import { Types, MAX_ROOM_MESSAGES } from './rooms.types';
|
||||
import { Actions } from './rooms.actions';
|
||||
import { MAX_ROOM_MESSAGES } from './rooms.types';
|
||||
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
||||
|
||||
// ── Initialisation ───────────────────────────────────────────────────────────
|
||||
|
|
@ -14,7 +15,7 @@ describe('Initialisation', () => {
|
|||
|
||||
it('CLEAR_STORE → resets to initialState', () => {
|
||||
const state = makeRoomsState({ joinedRoomIds: { 1: true } });
|
||||
const result = roomsReducer(state, { type: Types.CLEAR_STORE });
|
||||
const result = roomsReducer(state, Actions.clearStore());
|
||||
expect(result.joinedRoomIds).toEqual({});
|
||||
expect(result.rooms).toEqual({});
|
||||
});
|
||||
|
|
@ -22,63 +23,77 @@ describe('Initialisation', () => {
|
|||
it('default → returns state unchanged for unknown action', () => {
|
||||
const state = makeRoomsState();
|
||||
const result = roomsReducer(state, { type: '@@UNKNOWN' });
|
||||
expect(result).toBe(state);
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UPDATE_ROOMS ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('UPDATE_ROOMS', () => {
|
||||
it('merges rooms and strips gameList, gametypeList, userList from update', () => {
|
||||
it('creates RoomEntry with empty normalized games/users for new room', () => {
|
||||
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] });
|
||||
// UPDATE_ROOMS carries raw ServerInfo_Room protos via the action
|
||||
const room = makeRoom({ roomId: 1 }).info;
|
||||
const result = roomsReducer(state, Actions.updateRooms({ 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();
|
||||
expect(result.rooms[1].info).toBe(room);
|
||||
expect(result.rooms[1].games).toEqual({});
|
||||
expect(result.rooms[1].users).toEqual({});
|
||||
});
|
||||
|
||||
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 });
|
||||
const rooms = [makeRoom({ roomId: 1 }).info, makeRoom({ roomId: 2 }).info];
|
||||
const result = roomsReducer(state, Actions.updateRooms({ 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()] });
|
||||
it('preserves existing normalized games/users when merging into existing room', () => {
|
||||
const existingGame = makeGame({ gameId: 42 });
|
||||
const existingUser = makeUser({ name: 'alice' });
|
||||
const existingRoom = makeRoom({
|
||||
roomId: 1,
|
||||
name: 'Old Name',
|
||||
games: { 42: existingGame },
|
||||
users: { alice: existingUser },
|
||||
});
|
||||
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()]);
|
||||
|
||||
const update = makeRoom({ roomId: 1, name: 'New Name' }).info;
|
||||
const result = roomsReducer(state, Actions.updateRooms({ rooms: [update] }));
|
||||
|
||||
expect(result.rooms[1].info.name).toBe('New Name');
|
||||
expect(result.rooms[1].games[42]).toBe(existingGame);
|
||||
expect(result.rooms[1].users['alice']).toBe(existingUser);
|
||||
});
|
||||
|
||||
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] });
|
||||
const room = makeRoom({ roomId: 99, name: 'New Room' }).info;
|
||||
const result = roomsReducer(state, Actions.updateRooms({ rooms: [room] }));
|
||||
expect(result.rooms[99]).toBeDefined();
|
||||
expect(result.rooms[99].name).toBe('New Room');
|
||||
expect(result.rooms[99].info.name).toBe('New Room');
|
||||
});
|
||||
});
|
||||
|
||||
// ── JOIN_ROOM ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('JOIN_ROOM', () => {
|
||||
it('copies gameList and userList, sorts both, sets joinedRoomIds', () => {
|
||||
it('normalizes raw room into keyed games/users maps and marks joined', () => {
|
||||
const state = makeRoomsState({ rooms: {}, joinedRoomIds: {} });
|
||||
const roomInfo = makeRoom({
|
||||
// JOIN_ROOM carries a raw proto Room with its gameList/userList populated
|
||||
const rawRoom = makeRoom({
|
||||
roomId: 2,
|
||||
gameList: [makeGame({ gameId: 1 })],
|
||||
gameList: [makeGame({ gameId: 1 }).info],
|
||||
userList: [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })],
|
||||
});
|
||||
const result = roomsReducer(state, { type: Types.JOIN_ROOM, roomInfo });
|
||||
}).info;
|
||||
const result = roomsReducer(state, Actions.joinRoom({ roomInfo: rawRoom }));
|
||||
expect(result.joinedRoomIds[2]).toBe(true);
|
||||
expect(result.rooms[2].userList[0].name).toBe('Alice');
|
||||
expect(result.rooms[2]).toMatchObject({ roomId: 2 });
|
||||
expect(result.rooms[2].users['Alice']).toBeDefined();
|
||||
expect(result.rooms[2].users['Zane']).toBeDefined();
|
||||
expect(result.rooms[2].games[1]).toBeDefined();
|
||||
expect(result.rooms[2].info.roomId).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +105,7 @@ describe('LEAVE_ROOM', () => {
|
|||
joinedRoomIds: { 1: true },
|
||||
messages: { 1: [makeMessage()] },
|
||||
});
|
||||
const result = roomsReducer(state, { type: Types.LEAVE_ROOM, roomId: 1 });
|
||||
const result = roomsReducer(state, Actions.leaveRoom({ roomId: 1 }));
|
||||
expect(result.joinedRoomIds[1]).toBeUndefined();
|
||||
expect(result.messages[1]).toBeUndefined();
|
||||
});
|
||||
|
|
@ -102,7 +117,7 @@ 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 });
|
||||
const result = roomsReducer(state, Actions.addMessage({ roomId: 1, message }));
|
||||
expect(result.messages[1]).toHaveLength(1);
|
||||
expect(result.messages[1][0].timeReceived).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
@ -110,7 +125,7 @@ describe('ADD_MESSAGE', () => {
|
|||
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 });
|
||||
const result = roomsReducer(state, Actions.addMessage({ roomId: 5, message }));
|
||||
expect(result.messages[5]).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -121,7 +136,7 @@ describe('ADD_MESSAGE', () => {
|
|||
);
|
||||
const state = makeRoomsState({ messages: { 1: messages } });
|
||||
const newMsg = makeMessage({ message: 'new' });
|
||||
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message: newMsg });
|
||||
const result = roomsReducer(state, Actions.addMessage({ 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');
|
||||
|
|
@ -130,14 +145,14 @@ describe('ADD_MESSAGE', () => {
|
|||
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 });
|
||||
const result = roomsReducer(state, Actions.addMessage({ 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 });
|
||||
const result = roomsReducer(state, Actions.addMessage({ roomId: 1, message }));
|
||||
expect(result.messages[1][0].message).toBe('system msg');
|
||||
});
|
||||
});
|
||||
|
|
@ -145,94 +160,92 @@ describe('ADD_MESSAGE', () => {
|
|||
// ── UPDATE_GAMES ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('UPDATE_GAMES', () => {
|
||||
it('removes closed games from gameList', () => {
|
||||
const room = makeRoom({ roomId: 1, gameList: [makeGame({ gameId: 1 })] });
|
||||
it('removes closed games from the keyed games map', () => {
|
||||
const room = makeRoom({ roomId: 1, games: { 1: makeGame({ gameId: 1 }) } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
const result = roomsReducer(state, Actions.updateGames({
|
||||
roomId: 1,
|
||||
games: [{ gameId: 1, closed: true }],
|
||||
});
|
||||
expect(result.rooms[1].gameList).toHaveLength(0);
|
||||
}));
|
||||
expect(result.rooms[1].games[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('merges update into existing game', () => {
|
||||
it('merges update into existing game info', () => {
|
||||
const game = makeGame({ gameId: 1, description: 'old' });
|
||||
const room = makeRoom({ roomId: 1, gameList: [game] });
|
||||
const room = makeRoom({ roomId: 1, games: { 1: game } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
const result = roomsReducer(state, Actions.updateGames({
|
||||
roomId: 1,
|
||||
games: [{ gameId: 1, description: 'new' }],
|
||||
});
|
||||
expect(result.rooms[1].gameList[0].description).toBe('new');
|
||||
}));
|
||||
expect(result.rooms[1].games[1].info.description).toBe('new');
|
||||
});
|
||||
|
||||
it('appends new game to list and sorts', () => {
|
||||
const room = makeRoom({ roomId: 1, gameList: [] });
|
||||
it('inserts new game into the keyed map', () => {
|
||||
const room = makeRoom({ roomId: 1 });
|
||||
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);
|
||||
const newGame = makeGame({ gameId: 99, description: 'extra' }).info;
|
||||
const result = roomsReducer(state, Actions.updateGames({ roomId: 1, games: [newGame] }));
|
||||
expect(Object.keys(result.rooms[1].games)).toHaveLength(1);
|
||||
expect(result.rooms[1].games[99]).toBeDefined();
|
||||
expect(result.rooms[1].games[99].info.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 room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
const result = roomsReducer(state, Actions.updateGames({
|
||||
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');
|
||||
}));
|
||||
expect(result.rooms[1].games[1].info.description).toBe('untouched');
|
||||
expect(result.rooms[1].games[2].info.description).toBe('new');
|
||||
});
|
||||
|
||||
it('returns state identity when roomId is unknown', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 999, games: [] });
|
||||
expect(result).toBe(state);
|
||||
const result = roomsReducer(state, Actions.updateGames({ roomId: 999, games: [] }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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' })] });
|
||||
it('inserts user into the keyed users map', () => {
|
||||
const room = makeRoom({ roomId: 1, users: { Zane: 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);
|
||||
const result = roomsReducer(state, Actions.userJoined({ roomId: 1, user: makeUser({ name: 'Alice' }) }));
|
||||
expect(result.rooms[1].users['Alice']).toBeDefined();
|
||||
expect(result.rooms[1].users['Zane']).toBeDefined();
|
||||
expect(Object.keys(result.rooms[1].users)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('USER_LEFT', () => {
|
||||
it('removes user by name from userList', () => {
|
||||
const room = makeRoom({ roomId: 1, userList: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
|
||||
it('removes user by name from the keyed users map', () => {
|
||||
const room = makeRoom({
|
||||
roomId: 1,
|
||||
users: { Alice: makeUser({ name: 'Alice' }), Bob: 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');
|
||||
const result = roomsReducer(state, Actions.userLeft({ roomId: 1, name: 'Alice' }));
|
||||
expect(result.rooms[1].users['Alice']).toBeUndefined();
|
||||
expect(result.rooms[1].users['Bob']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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,
|
||||
it('updates sortGamesBy on state (sorting itself is now derived in selectors)', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const result = roomsReducer(state, Actions.sortGames({
|
||||
field: App.GameSortField.START_TIME,
|
||||
order: App.SortDirection.ASC,
|
||||
});
|
||||
}));
|
||||
expect(result.sortGamesBy).toEqual({ field: App.GameSortField.START_TIME, order: App.SortDirection.ASC });
|
||||
});
|
||||
});
|
||||
|
|
@ -247,7 +260,7 @@ describe('REMOVE_MESSAGES', () => {
|
|||
makeMessage({ message: 'Alice: world' }),
|
||||
];
|
||||
const state = makeRoomsState({ messages: { 1: msgs } });
|
||||
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
|
||||
const result = roomsReducer(state, Actions.removeMessages({ 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);
|
||||
|
|
@ -263,7 +276,7 @@ describe('REMOVE_MESSAGES', () => {
|
|||
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 result = roomsReducer(state, Actions.removeMessages({ roomId: 1, name: 'Alice', amount: 2 }));
|
||||
const remaining = result.messages[1];
|
||||
expect(remaining).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -275,7 +288,7 @@ describe('REMOVE_MESSAGES', () => {
|
|||
makeMessage({ message: 'Alice: c' }),
|
||||
];
|
||||
const state = makeRoomsState({ messages: { 1: msgs } });
|
||||
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
|
||||
const result = roomsReducer(state, Actions.removeMessages({ roomId: 1, name: 'Alice', amount: 1 }));
|
||||
expect(result.messages[1]).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -285,8 +298,8 @@ describe('REMOVE_MESSAGES', () => {
|
|||
describe('GAME_CREATED', () => {
|
||||
it('returns state unchanged', () => {
|
||||
const state = makeRoomsState();
|
||||
const result = roomsReducer(state, { type: Types.GAME_CREATED, roomId: 1 });
|
||||
expect(result).toBe(state);
|
||||
const result = roomsReducer(state, Actions.gameCreated({ roomId: 1 }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -295,13 +308,13 @@ describe('GAME_CREATED', () => {
|
|||
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 });
|
||||
const result = roomsReducer(state, Actions.joinedGame({ 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 });
|
||||
const result = roomsReducer(state, Actions.joinedGame({ roomId: 1, gameId: 5 }));
|
||||
expect(result.joinedGameIds[2][9]).toBe(true);
|
||||
expect(result.joinedGameIds[1][5]).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import * as _ from 'lodash';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { App, Data, Enriched } from '@app/types';
|
||||
|
||||
import { App, Enriched } from '@app/types';
|
||||
import { normalizeGameObject, normalizeGametypeMap, normalizeRoomInfo, normalizeUserMessage } from '../common';
|
||||
|
||||
import { normalizeGameObject, normalizeGametypeMap, normalizeRoomInfo, normalizeUserMessage, SortUtil } from '../common';
|
||||
|
||||
import { RoomsAction } from './rooms.actions';
|
||||
import { RoomsState } from './rooms.interfaces'
|
||||
import { MAX_ROOM_MESSAGES, Types } from './rooms.types';
|
||||
|
||||
export const MAX_ROOM_MESSAGES = 1000;
|
||||
|
||||
const initialState: RoomsState = {
|
||||
rooms: {},
|
||||
games: {},
|
||||
joinedRoomIds: {},
|
||||
joinedGameIds: {},
|
||||
messages: {},
|
||||
|
|
@ -24,316 +22,163 @@ const initialState: RoomsState = {
|
|||
}
|
||||
};
|
||||
|
||||
export const roomsReducer = (state = initialState, action: RoomsAction) => {
|
||||
switch (action.type) {
|
||||
case Types.CLEAR_STORE: {
|
||||
return {
|
||||
...initialState
|
||||
};
|
||||
}
|
||||
export const roomsSlice = createSlice({
|
||||
name: 'rooms',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearStore: () => initialState,
|
||||
|
||||
case Types.UPDATE_ROOMS: {
|
||||
const rooms = {
|
||||
...state.rooms
|
||||
};
|
||||
updateRooms: (state, action: PayloadAction<{ rooms: Data.ServerInfo_Room[] }>) => {
|
||||
const { rooms } = action.payload;
|
||||
|
||||
// Server does not send everything on updates — preserve existing gameList/userList
|
||||
_.each(action.rooms, (rawRoom, order) => {
|
||||
const { gameList: _g, gametypeList, userList: _u, ...roomMeta } = rawRoom;
|
||||
const { roomId } = roomMeta;
|
||||
const existing = rooms[roomId] || {};
|
||||
// UPDATE_ROOMS carries metadata only. For existing rooms, replace
|
||||
// `info`, `gametypeMap` and `order`; preserve the normalized `games`
|
||||
// and `users` maps (those are maintained by their own events).
|
||||
rooms.forEach((rawRoom, order) => {
|
||||
const { roomId } = rawRoom;
|
||||
const existing = state.rooms[roomId];
|
||||
const gametypeMap = normalizeGametypeMap(rawRoom.gametypeList);
|
||||
|
||||
const gametypeMap = normalizeGametypeMap(gametypeList);
|
||||
|
||||
rooms[roomId] = {
|
||||
...(existing as Enriched.Room),
|
||||
...roomMeta,
|
||||
gametypeMap,
|
||||
gameList: (existing as Enriched.Room).gameList,
|
||||
userList: (existing as Enriched.Room).userList,
|
||||
order,
|
||||
};
|
||||
if (existing) {
|
||||
existing.info = rawRoom;
|
||||
existing.gametypeMap = gametypeMap;
|
||||
existing.order = order;
|
||||
} else {
|
||||
state.rooms[roomId] = {
|
||||
info: rawRoom,
|
||||
gametypeMap,
|
||||
order,
|
||||
games: {},
|
||||
users: {},
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
return { ...state, rooms };
|
||||
}
|
||||
joinRoom: (state, action: PayloadAction<{ roomInfo: Data.ServerInfo_Room }>) => {
|
||||
const { roomInfo: rawRoomInfo } = action.payload;
|
||||
|
||||
case Types.JOIN_ROOM: {
|
||||
const { roomInfo: rawRoomInfo } = action;
|
||||
const { joinedRoomIds, rooms, sortGamesBy, sortUsersBy } = state;
|
||||
const roomEntry = normalizeRoomInfo(rawRoomInfo);
|
||||
const roomId = roomEntry.info.roomId;
|
||||
|
||||
const roomInfo = normalizeRoomInfo(rawRoomInfo);
|
||||
const { roomId } = roomInfo;
|
||||
state.rooms[roomId] = roomEntry;
|
||||
state.joinedRoomIds[roomId] = true;
|
||||
},
|
||||
|
||||
const gameList = [
|
||||
...roomInfo.gameList
|
||||
];
|
||||
leaveRoom: (state, action: PayloadAction<{ roomId: number }>) => {
|
||||
const { roomId } = action.payload;
|
||||
|
||||
const userList = [
|
||||
...roomInfo.userList
|
||||
];
|
||||
delete state.joinedRoomIds[roomId];
|
||||
delete state.messages[roomId];
|
||||
},
|
||||
|
||||
SortUtil.sortByField(gameList, sortGamesBy);
|
||||
SortUtil.sortUsersByField(userList, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
rooms: {
|
||||
...rooms,
|
||||
[roomId]: {
|
||||
...roomInfo,
|
||||
gameList,
|
||||
userList
|
||||
}
|
||||
},
|
||||
|
||||
joinedRoomIds: {
|
||||
...joinedRoomIds,
|
||||
[roomId]: true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case Types.LEAVE_ROOM: {
|
||||
const { roomId } = action;
|
||||
const { joinedRoomIds, messages } = state;
|
||||
|
||||
const _joined = {
|
||||
...joinedRoomIds
|
||||
};
|
||||
|
||||
const _messages = {
|
||||
...messages
|
||||
};
|
||||
|
||||
delete _joined[roomId];
|
||||
delete _messages[roomId];
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
joinedRoomIds: _joined,
|
||||
messages: _messages,
|
||||
}
|
||||
}
|
||||
|
||||
case Types.ADD_MESSAGE: {
|
||||
const { roomId, message } = action;
|
||||
const { messages } = state;
|
||||
|
||||
let roomMessages = [...(messages[roomId] || [])];
|
||||
|
||||
if (roomMessages.length === MAX_ROOM_MESSAGES) {
|
||||
roomMessages.shift();
|
||||
}
|
||||
addMessage: (state, action: PayloadAction<{ roomId: number; message: Enriched.Message }>) => {
|
||||
const { roomId, message } = action.payload;
|
||||
|
||||
const existing = state.messages[roomId] ?? [];
|
||||
const normalized = normalizeUserMessage({ ...message, timeReceived: Date.now() });
|
||||
roomMessages.push(normalized);
|
||||
const next =
|
||||
existing.length >= MAX_ROOM_MESSAGES
|
||||
? [...existing.slice(existing.length - MAX_ROOM_MESSAGES + 1), normalized]
|
||||
: [...existing, normalized];
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...messages,
|
||||
state.messages[roomId] = next;
|
||||
},
|
||||
|
||||
[roomId]: [
|
||||
...roomMessages
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
// @TODO improve this reducer, likely by improving the store model
|
||||
updateGames: (state, action: PayloadAction<{ roomId: number; games: Data.ServerInfo_Game[] }>) => {
|
||||
const { roomId, games } = action.payload;
|
||||
const room = state.rooms[roomId];
|
||||
|
||||
case Types.UPDATE_GAMES: {
|
||||
const { roomId, games } = action;
|
||||
const { rooms, sortGamesBy } = state;
|
||||
const room = rooms[roomId];
|
||||
|
||||
// An empty gameList means no game updates — skip to avoid
|
||||
// overwriting the existing game list with an empty one.
|
||||
// An empty games array means no game updates — skip to avoid
|
||||
// accidentally wiping the existing normalized games map.
|
||||
if (!room || !games?.length) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize incoming raw proto games using the room's gametypeMap
|
||||
const gametypeMap = room.gametypeMap ?? {};
|
||||
const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap));
|
||||
|
||||
// Create map of games with update objects
|
||||
const toUpdate = normalizedGames.reduce((map, game) => {
|
||||
map[game.gameId] = game;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const gameUpdates = room.gameList
|
||||
// filter out closed games and remove from update map
|
||||
.filter(game => {
|
||||
const gameUpdate = toUpdate[game.gameId];
|
||||
const closedGame = gameUpdate && gameUpdate.closed;
|
||||
|
||||
if (closedGame) {
|
||||
delete toUpdate[game.gameId];
|
||||
}
|
||||
|
||||
return !closedGame;
|
||||
})
|
||||
.map(game => {
|
||||
const gameUpdate = toUpdate[game.gameId];
|
||||
|
||||
if (gameUpdate) {
|
||||
delete toUpdate[game.gameId];
|
||||
|
||||
return {
|
||||
...game,
|
||||
...gameUpdate
|
||||
};
|
||||
}
|
||||
|
||||
return game;
|
||||
});
|
||||
|
||||
// Push new games to end of list
|
||||
if (_.size(toUpdate)) {
|
||||
_.each(toUpdate, game => gameUpdates.push(game));
|
||||
}
|
||||
|
||||
const gameList = [...gameUpdates];
|
||||
|
||||
SortUtil.sortByField(gameList, sortGamesBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
rooms: {
|
||||
...rooms,
|
||||
[roomId]: {
|
||||
...room,
|
||||
gameList
|
||||
}
|
||||
for (const rawGame of games) {
|
||||
if (rawGame.closed) {
|
||||
delete room.games[rawGame.gameId];
|
||||
continue;
|
||||
}
|
||||
const existing = room.games[rawGame.gameId];
|
||||
if (existing) {
|
||||
// Merge the incoming proto into the existing snapshot.
|
||||
const merged: Data.ServerInfo_Game = { ...existing.info, ...rawGame };
|
||||
room.games[rawGame.gameId] = {
|
||||
info: merged,
|
||||
gameType: merged.gameTypes?.length
|
||||
? (gametypeMap[merged.gameTypes[0]] ?? '')
|
||||
: existing.gameType,
|
||||
};
|
||||
} else {
|
||||
room.games[rawGame.gameId] = normalizeGameObject(rawGame, gametypeMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
case Types.USER_JOINED: {
|
||||
const { roomId, user } = action;
|
||||
const { rooms, sortUsersBy } = state;
|
||||
|
||||
const room = { ...rooms[roomId] };
|
||||
|
||||
const userList = [
|
||||
...room.userList,
|
||||
user
|
||||
];
|
||||
|
||||
SortUtil.sortUsersByField(userList, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
rooms: {
|
||||
...rooms,
|
||||
[roomId]: {
|
||||
...room,
|
||||
userList
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case Types.USER_LEFT: {
|
||||
const { roomId, name } = action;
|
||||
const { rooms } = state;
|
||||
|
||||
const room = { ...rooms[roomId] };
|
||||
const userList = room.userList.filter(user => user.name !== name);
|
||||
|
||||
return {
|
||||
...state,
|
||||
rooms: {
|
||||
...rooms,
|
||||
[roomId]: {
|
||||
...room,
|
||||
userList
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case Types.SORT_GAMES: {
|
||||
const { field, order, roomId } = action;
|
||||
const { rooms } = state;
|
||||
|
||||
const gameList = [...rooms[roomId].gameList];
|
||||
|
||||
const sortGamesBy = {
|
||||
field, order
|
||||
};
|
||||
|
||||
SortUtil.sortByField(gameList, sortGamesBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
rooms: {
|
||||
...rooms,
|
||||
[roomId]: {
|
||||
...rooms[roomId],
|
||||
gameList
|
||||
}
|
||||
},
|
||||
|
||||
sortGamesBy
|
||||
userJoined: (state, action: PayloadAction<{ roomId: number; user: Data.ServerInfo_User }>) => {
|
||||
const { roomId, user } = action.payload;
|
||||
const room = state.rooms[roomId];
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
case Types.REMOVE_MESSAGES: {
|
||||
const { name, amount, roomId } = action;
|
||||
const { messages } = state;
|
||||
let amountRemoved = 0;
|
||||
room.users[user.name] = user;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...messages,
|
||||
[roomId]: messages[roomId]
|
||||
.reverse()
|
||||
.filter(({ message }) => {
|
||||
if (amount === amountRemoved) {
|
||||
return true;
|
||||
}
|
||||
userLeft: (state, action: PayloadAction<{ roomId: number; name: string }>) => {
|
||||
const { roomId, name } = action.payload;
|
||||
const room = state.rooms[roomId];
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keep = message.indexOf(`${name}:`) !== 0;
|
||||
delete room.users[name];
|
||||
},
|
||||
|
||||
if (!keep) {
|
||||
amountRemoved++;
|
||||
}
|
||||
sortGames: (state, action: PayloadAction<{ field: App.GameSortField; order: App.SortDirection }>) => {
|
||||
// Sort is now derived in selectors; the reducer only stores the sort config.
|
||||
const { field, order } = action.payload;
|
||||
state.sortGamesBy = { field, order };
|
||||
},
|
||||
|
||||
return keep;
|
||||
})
|
||||
.reverse()
|
||||
removeMessages: (state, action: PayloadAction<{ roomId: number; name: string; amount: number }>) => {
|
||||
const { name, amount, roomId } = action.payload;
|
||||
const roomMessages = state.messages[roomId];
|
||||
|
||||
if (!roomMessages) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop the `amount` most-recent messages whose text starts with `${name}:`.
|
||||
// Walk newest → oldest so we remove the N latest matches.
|
||||
const prefix = `${name}:`;
|
||||
const keep = new Array(roomMessages.length).fill(true);
|
||||
let remaining = amount;
|
||||
for (let i = roomMessages.length - 1; i >= 0 && remaining > 0; i--) {
|
||||
if (roomMessages[i].message.indexOf(prefix) === 0) {
|
||||
keep[i] = false;
|
||||
remaining--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case Types.JOINED_GAME: {
|
||||
const { gameId, roomId } = action;
|
||||
const { joinedGameIds } = state;
|
||||
state.messages[roomId] = roomMessages.filter((_, i) => keep[i]);
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
joinedGameIds: {
|
||||
...joinedGameIds,
|
||||
[roomId]: {
|
||||
...joinedGameIds[roomId],
|
||||
[gameId]: true,
|
||||
}
|
||||
}
|
||||
joinedGame: (state, action: PayloadAction<{ gameId: number; roomId: number }>) => {
|
||||
const { gameId, roomId } = action.payload;
|
||||
|
||||
if (!state.joinedGameIds[roomId]) {
|
||||
state.joinedGameIds[roomId] = {};
|
||||
}
|
||||
}
|
||||
state.joinedGameIds[roomId][gameId] = true;
|
||||
},
|
||||
|
||||
// Signal-only — no state mutation needed; explicit for discriminated-union exhaustiveness
|
||||
case Types.GAME_CREATED:
|
||||
return state;
|
||||
gameCreated: (_state, _action: PayloadAction<{ roomId: number }>) => {},
|
||||
},
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
export const roomsReducer = roomsSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@ describe('Selectors', () => {
|
|||
expect(Selectors.getRooms(rootState(state))).toBe(state.rooms);
|
||||
});
|
||||
|
||||
it('getGames → returns games map', () => {
|
||||
const state = makeRoomsState({ games: { 1: { 1: makeGame() } } });
|
||||
expect(Selectors.getGames(rootState(state))).toBe(state.games);
|
||||
});
|
||||
|
||||
it('getRoom → returns room matching roomId', () => {
|
||||
const room = makeRoom({ roomId: 1 });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
|
|
@ -76,8 +71,9 @@ describe('Selectors', () => {
|
|||
it('getJoinedGames → returns only games whose gameId is in joinedGameIds for that room', () => {
|
||||
const game1 = makeGame({ gameId: 1 });
|
||||
const game2 = makeGame({ gameId: 2 });
|
||||
const room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } });
|
||||
const state = makeRoomsState({
|
||||
games: { 1: { 1: game1, 2: game2 } },
|
||||
rooms: { 1: room },
|
||||
joinedGameIds: { 1: { 1: true } },
|
||||
});
|
||||
const result = Selectors.getJoinedGames(rootState(state), 1);
|
||||
|
|
@ -85,21 +81,52 @@ describe('Selectors', () => {
|
|||
expect(result[0]).toBe(game1);
|
||||
});
|
||||
|
||||
it('getJoinedGames → returns empty array when room is unknown', () => {
|
||||
const state = makeRoomsState({ rooms: {}, joinedGameIds: { 1: { 1: true } } });
|
||||
expect(Selectors.getJoinedGames(rootState(state), 1)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('getRoomMessages → returns messages array for roomId', () => {
|
||||
const messages = [makeMessage()];
|
||||
const state = makeRoomsState({ messages: { 1: messages } });
|
||||
expect(Selectors.getRoomMessages(rootState(state), 1)).toBe(messages);
|
||||
});
|
||||
|
||||
it('getRoomGames → returns gameList for roomId', () => {
|
||||
const room = makeRoom({ roomId: 1, gameList: [makeGame()] });
|
||||
it('getRoomGames → returns keyed games map for roomId', () => {
|
||||
const game = makeGame({ gameId: 10 });
|
||||
const room = makeRoom({ roomId: 1, games: { 10: game } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
expect(Selectors.getRoomGames(rootState(state), 1)).toBe(room.gameList);
|
||||
expect(Selectors.getRoomGames(rootState(state), 1)).toBe(room.games);
|
||||
});
|
||||
|
||||
it('getRoomUsers → returns userList for roomId', () => {
|
||||
const room = makeRoom({ roomId: 1, userList: [makeUser()] });
|
||||
it('getRoomGames → returns EMPTY_GAMES_MAP for unknown roomId', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
expect(Selectors.getRoomGames(rootState(state), 999)).toEqual({});
|
||||
});
|
||||
|
||||
it('getRoomUsers → returns keyed users map for roomId', () => {
|
||||
const user = makeUser({ name: 'alice' });
|
||||
const room = makeRoom({ roomId: 1, users: { alice: user } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.userList);
|
||||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.users);
|
||||
});
|
||||
|
||||
it('getSortedRoomGames → returns sorted array view of games map', () => {
|
||||
const game1 = makeGame({ gameId: 1, description: 'beta' });
|
||||
const game2 = makeGame({ gameId: 2, description: 'alpha' });
|
||||
const room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = Selectors.getSortedRoomGames(rootState(state), 1);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('getSortedRoomUsers → returns sorted user array sorted by name', () => {
|
||||
const zane = makeUser({ name: 'Zane' });
|
||||
const alice = makeUser({ name: 'Alice' });
|
||||
const room = makeRoom({ roomId: 1, users: { Zane: zane, Alice: alice } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = Selectors.getSortedRoomUsers(rootState(state), 1);
|
||||
expect(result[0].name).toBe('Alice');
|
||||
expect(result[1].name).toBe('Zane');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import * as _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { Data, Enriched } from '@app/types';
|
||||
import { SortUtil } from '../common';
|
||||
import { RoomsState } from './rooms.interfaces';
|
||||
|
||||
interface State {
|
||||
rooms: RoomsState
|
||||
}
|
||||
|
||||
const EMPTY_GAMES: Enriched.Game[] = [];
|
||||
const EMPTY_USERS: Data.ServerInfo_User[] = [];
|
||||
const EMPTY_GAMES_MAP: { [id: number]: Enriched.Game } = {};
|
||||
const EMPTY_USERS_MAP: { [name: string]: Data.ServerInfo_User } = {};
|
||||
|
||||
export const Selectors = {
|
||||
getRooms: ({ rooms }: State) => rooms.rooms,
|
||||
getGames: ({ rooms }: State) => rooms.games,
|
||||
getRoom: ({ rooms }: State, id: number) =>
|
||||
_.find(rooms.rooms, ({ roomId }) => roomId === id),
|
||||
getRoom: ({ rooms }: State, id: number) => rooms.rooms[id],
|
||||
getJoinedRoomIds: ({ rooms }: State) => rooms.joinedRoomIds,
|
||||
getJoinedGameIds: ({ rooms }: State) => rooms.joinedGameIds,
|
||||
getMessages: ({ rooms }: State) => rooms.messages,
|
||||
|
|
@ -19,17 +23,60 @@ export const Selectors = {
|
|||
|
||||
getJoinedRooms: createSelector(
|
||||
[(state: State) => state.rooms.rooms, (state: State) => state.rooms.joinedRoomIds],
|
||||
(rooms, joined) => _.filter(rooms, room => joined[room.roomId])
|
||||
(rooms, joined) => Object.values(rooms).filter(room => joined[room.info.roomId])
|
||||
),
|
||||
|
||||
getJoinedGames: createSelector(
|
||||
[(state: State, roomId: number) => state.rooms.games[roomId], (state: State, roomId: number) => state.rooms.joinedGameIds[roomId]],
|
||||
(games, joined) => _.filter(games, game => joined[game.gameId])
|
||||
/**
|
||||
* Returns games in the given room that the local client has joined.
|
||||
* Reads from the room's normalized `games` map — fixes the pre-existing
|
||||
* bug where this selector read from a never-populated top-level `games` field.
|
||||
*/
|
||||
getJoinedGames: (state: State, roomId: number): Enriched.Game[] => {
|
||||
const room = state.rooms.rooms[roomId];
|
||||
const joined = state.rooms.joinedGameIds[roomId];
|
||||
if (!room || !joined) {
|
||||
return EMPTY_GAMES;
|
||||
}
|
||||
return Object.values(room.games).filter(game => joined[game.info.gameId]);
|
||||
},
|
||||
|
||||
getRoomMessages: (state: State, roomId: number) => state.rooms.messages[roomId],
|
||||
|
||||
/** Raw keyed games map for a room. For a sorted array, use `getSortedRoomGames`. */
|
||||
getRoomGames: (state: State, roomId: number) => state.rooms.rooms[roomId]?.games ?? EMPTY_GAMES_MAP,
|
||||
|
||||
/** Raw keyed users map for a room. For a sorted array, use `getSortedRoomUsers`. */
|
||||
getRoomUsers: (state: State, roomId: number) => state.rooms.rooms[roomId]?.users ?? EMPTY_USERS_MAP,
|
||||
|
||||
/**
|
||||
* Sorted array view of a room's games for display. Memoized by the input
|
||||
* references — recomputes only when the games map, gametypeMap, or sort
|
||||
* config actually change.
|
||||
*/
|
||||
getSortedRoomGames: createSelector(
|
||||
[
|
||||
(state: State, roomId: number) => state.rooms.rooms[roomId]?.games,
|
||||
(state: State) => state.rooms.sortGamesBy,
|
||||
],
|
||||
(games, sortBy): Enriched.Game[] => {
|
||||
if (!games) {
|
||||
return EMPTY_GAMES;
|
||||
}
|
||||
return SortUtil.sortedByField(Object.values(games), sortBy);
|
||||
}
|
||||
),
|
||||
|
||||
getRoomMessages: (state: State, roomId: number) => Selectors.getMessages(state)[roomId],
|
||||
getRoomGames: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].gameList,
|
||||
getRoomUsers: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].userList
|
||||
|
||||
/** Sorted array view of a room's users for display. */
|
||||
getSortedRoomUsers: createSelector(
|
||||
[
|
||||
(state: State, roomId: number) => state.rooms.rooms[roomId]?.users,
|
||||
(state: State) => state.rooms.sortUsersBy,
|
||||
],
|
||||
(users, sortBy): Data.ServerInfo_User[] => {
|
||||
if (!users) {
|
||||
return EMPTY_USERS;
|
||||
}
|
||||
return SortUtil.sortedUsersByField(Object.values(users), sortBy);
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import { roomsSlice } from './rooms.reducer';
|
||||
|
||||
const a = roomsSlice.actions;
|
||||
|
||||
export const Types = {
|
||||
CLEAR_STORE: '[Rooms] Clear Store',
|
||||
UPDATE_ROOMS: '[Rooms] Update Rooms',
|
||||
JOIN_ROOM: '[Rooms] Join Room',
|
||||
LEAVE_ROOM: '[Rooms] Leave Room',
|
||||
ADD_MESSAGE: '[Rooms] Add Message',
|
||||
UPDATE_GAMES: '[Rooms] Update Games',
|
||||
USER_JOINED: '[Rooms] User Joined',
|
||||
USER_LEFT: '[Rooms] User Left',
|
||||
SORT_GAMES: '[Rooms] Sort Games',
|
||||
REMOVE_MESSAGES: '[Rooms] Remove Messages',
|
||||
GAME_CREATED: '[Rooms] Game Created',
|
||||
JOINED_GAME: '[Rooms] Joined Game',
|
||||
CLEAR_STORE: a.clearStore.type,
|
||||
UPDATE_ROOMS: a.updateRooms.type,
|
||||
JOIN_ROOM: a.joinRoom.type,
|
||||
LEAVE_ROOM: a.leaveRoom.type,
|
||||
ADD_MESSAGE: a.addMessage.type,
|
||||
UPDATE_GAMES: a.updateGames.type,
|
||||
USER_JOINED: a.userJoined.type,
|
||||
USER_LEFT: a.userLeft.type,
|
||||
SORT_GAMES: a.sortGames.type,
|
||||
REMOVE_MESSAGES: a.removeMessages.type,
|
||||
GAME_CREATED: a.gameCreated.type,
|
||||
JOINED_GAME: a.joinedGame.type,
|
||||
} as const;
|
||||
|
||||
export const MAX_ROOM_MESSAGES = 1000;
|
||||
export { MAX_ROOM_MESSAGES } from './rooms.reducer';
|
||||
|
|
|
|||
|
|
@ -104,8 +104,16 @@ export function makeReplayMatch(
|
|||
});
|
||||
}
|
||||
|
||||
export function makeGame(overrides: Partial<Enriched.Game> = {}): Enriched.Game {
|
||||
return { ...create(Data.ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides };
|
||||
type MakeGameOverrides = MessageInitShape<typeof Data.ServerInfo_GameSchema> & {
|
||||
gameType?: string;
|
||||
};
|
||||
|
||||
export function makeGame(overrides: MakeGameOverrides = {}): Enriched.Game {
|
||||
const { gameType = '', ...protoFields } = overrides;
|
||||
return {
|
||||
info: create(Data.ServerInfo_GameSchema, { description: '', ...protoFields }),
|
||||
gameType,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeLoginSuccessContext(
|
||||
|
|
@ -131,8 +139,8 @@ export function makePendingActivationContext(
|
|||
export function makeServerState(overrides: Partial<ServerState> = {}): ServerState {
|
||||
return {
|
||||
initialized: false,
|
||||
buddyList: [],
|
||||
ignoreList: [],
|
||||
buddyList: {},
|
||||
ignoreList: {},
|
||||
status: {
|
||||
connectionAttemptMade: false,
|
||||
state: App.StatusEnum.DISCONNECTED,
|
||||
|
|
@ -149,7 +157,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
|
|||
chat: [],
|
||||
},
|
||||
user: null,
|
||||
users: [],
|
||||
users: {},
|
||||
sortUsersBy: {
|
||||
field: App.UserSortField.NAME,
|
||||
order: App.SortDirection.ASC,
|
||||
|
|
@ -164,7 +172,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
|
|||
warnListOptions: [],
|
||||
warnUser: '',
|
||||
adminNotes: {},
|
||||
replays: [],
|
||||
replays: {},
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
registrationError: null,
|
||||
|
|
|
|||
|
|
@ -16,349 +16,348 @@ import {
|
|||
|
||||
describe('Actions', () => {
|
||||
it('initialized', () => {
|
||||
expect(Actions.initialized()).toEqual({ type: Types.INITIALIZED });
|
||||
expect(Actions.initialized()).toEqual({ type: Types.INITIALIZED, payload: undefined });
|
||||
});
|
||||
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE, payload: undefined });
|
||||
});
|
||||
|
||||
it('connectionAttempted', () => {
|
||||
expect(Actions.connectionAttempted()).toEqual({ type: Types.CONNECTION_ATTEMPTED });
|
||||
expect(Actions.connectionAttempted()).toEqual({ type: Types.CONNECTION_ATTEMPTED, payload: undefined });
|
||||
});
|
||||
|
||||
it('loginSuccessful', () => {
|
||||
const options = makeLoginSuccessContext();
|
||||
expect(Actions.loginSuccessful(options)).toEqual({ type: Types.LOGIN_SUCCESSFUL, options });
|
||||
expect(Actions.loginSuccessful({ options })).toEqual({ type: Types.LOGIN_SUCCESSFUL, payload: { options } });
|
||||
});
|
||||
|
||||
it('loginFailed', () => {
|
||||
expect(Actions.loginFailed()).toEqual({ type: Types.LOGIN_FAILED });
|
||||
expect(Actions.loginFailed()).toEqual({ type: Types.LOGIN_FAILED, payload: undefined });
|
||||
});
|
||||
|
||||
it('connectionFailed', () => {
|
||||
expect(Actions.connectionFailed()).toEqual({ type: Types.CONNECTION_FAILED });
|
||||
expect(Actions.connectionFailed()).toEqual({ type: Types.CONNECTION_FAILED, payload: undefined });
|
||||
});
|
||||
|
||||
it('testConnectionSuccessful', () => {
|
||||
expect(Actions.testConnectionSuccessful()).toEqual({ type: Types.TEST_CONNECTION_SUCCESSFUL });
|
||||
expect(Actions.testConnectionSuccessful()).toEqual({ type: Types.TEST_CONNECTION_SUCCESSFUL, payload: undefined });
|
||||
});
|
||||
|
||||
it('testConnectionFailed', () => {
|
||||
expect(Actions.testConnectionFailed()).toEqual({ type: Types.TEST_CONNECTION_FAILED });
|
||||
expect(Actions.testConnectionFailed()).toEqual({ type: Types.TEST_CONNECTION_FAILED, payload: undefined });
|
||||
});
|
||||
|
||||
it('serverMessage', () => {
|
||||
expect(Actions.serverMessage('hello')).toEqual({ type: Types.SERVER_MESSAGE, message: 'hello' });
|
||||
expect(Actions.serverMessage({ message: 'hello' })).toEqual({ type: Types.SERVER_MESSAGE, payload: { message: 'hello' } });
|
||||
});
|
||||
|
||||
it('updateBuddyList', () => {
|
||||
const list = [makeUser()];
|
||||
expect(Actions.updateBuddyList(list)).toEqual({ type: Types.UPDATE_BUDDY_LIST, buddyList: list });
|
||||
expect(Actions.updateBuddyList({ buddyList: list })).toEqual({ type: Types.UPDATE_BUDDY_LIST, payload: { buddyList: list } });
|
||||
});
|
||||
|
||||
it('addToBuddyList', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.addToBuddyList(user)).toEqual({ type: Types.ADD_TO_BUDDY_LIST, user });
|
||||
expect(Actions.addToBuddyList({ user })).toEqual({ type: Types.ADD_TO_BUDDY_LIST, payload: { user } });
|
||||
});
|
||||
|
||||
it('removeFromBuddyList', () => {
|
||||
expect(Actions.removeFromBuddyList('Alice')).toEqual({ type: Types.REMOVE_FROM_BUDDY_LIST, userName: 'Alice' });
|
||||
const action = Actions.removeFromBuddyList({ userName: 'Alice' });
|
||||
expect(action).toEqual({ type: Types.REMOVE_FROM_BUDDY_LIST, payload: { userName: 'Alice' } });
|
||||
});
|
||||
|
||||
it('updateIgnoreList', () => {
|
||||
const list = [makeUser()];
|
||||
expect(Actions.updateIgnoreList(list)).toEqual({ type: Types.UPDATE_IGNORE_LIST, ignoreList: list });
|
||||
expect(Actions.updateIgnoreList({ ignoreList: list })).toEqual({ type: Types.UPDATE_IGNORE_LIST, payload: { ignoreList: list } });
|
||||
});
|
||||
|
||||
it('addToIgnoreList', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.addToIgnoreList(user)).toEqual({ type: Types.ADD_TO_IGNORE_LIST, user });
|
||||
expect(Actions.addToIgnoreList({ user })).toEqual({ type: Types.ADD_TO_IGNORE_LIST, payload: { user } });
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList', () => {
|
||||
expect(Actions.removeFromIgnoreList('Bob')).toEqual({ type: Types.REMOVE_FROM_IGNORE_LIST, userName: 'Bob' });
|
||||
const action = Actions.removeFromIgnoreList({ userName: 'Bob' });
|
||||
expect(action).toEqual({ type: Types.REMOVE_FROM_IGNORE_LIST, payload: { userName: 'Bob' } });
|
||||
});
|
||||
|
||||
it('updateInfo', () => {
|
||||
const info = { name: 'Servatrice', version: '2.0' };
|
||||
expect(Actions.updateInfo(info)).toEqual({ type: Types.UPDATE_INFO, info });
|
||||
expect(Actions.updateInfo({ info })).toEqual({ type: Types.UPDATE_INFO, payload: { info } });
|
||||
});
|
||||
|
||||
it('updateStatus', () => {
|
||||
const status = { state: App.StatusEnum.CONNECTED, description: 'connected' };
|
||||
expect(Actions.updateStatus(status)).toEqual({ type: Types.UPDATE_STATUS, status });
|
||||
expect(Actions.updateStatus({ status })).toEqual({ type: Types.UPDATE_STATUS, payload: { status } });
|
||||
});
|
||||
|
||||
it('updateUser', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.updateUser(user)).toEqual({ type: Types.UPDATE_USER, user });
|
||||
expect(Actions.updateUser({ user })).toEqual({ type: Types.UPDATE_USER, payload: { user } });
|
||||
});
|
||||
|
||||
it('updateUsers', () => {
|
||||
const users = [makeUser()];
|
||||
expect(Actions.updateUsers(users)).toEqual({ type: Types.UPDATE_USERS, users });
|
||||
expect(Actions.updateUsers({ users })).toEqual({ type: Types.UPDATE_USERS, payload: { users } });
|
||||
});
|
||||
|
||||
it('userJoined', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.userJoined(user)).toEqual({ type: Types.USER_JOINED, user });
|
||||
expect(Actions.userJoined({ user })).toEqual({ type: Types.USER_JOINED, payload: { user } });
|
||||
});
|
||||
|
||||
it('userLeft', () => {
|
||||
expect(Actions.userLeft('Carol')).toEqual({ type: Types.USER_LEFT, name: 'Carol' });
|
||||
expect(Actions.userLeft({ name: 'Carol' })).toEqual({ type: Types.USER_LEFT, payload: { name: 'Carol' } });
|
||||
});
|
||||
|
||||
it('viewLogs', () => {
|
||||
const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })];
|
||||
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
|
||||
expect(Actions.viewLogs({ logs })).toEqual({ type: Types.VIEW_LOGS, payload: { logs } });
|
||||
});
|
||||
|
||||
it('clearLogs', () => {
|
||||
expect(Actions.clearLogs()).toEqual({ type: Types.CLEAR_LOGS });
|
||||
expect(Actions.clearLogs()).toEqual({ type: Types.CLEAR_LOGS, payload: undefined });
|
||||
});
|
||||
|
||||
it('registrationRequiresEmail', () => {
|
||||
expect(Actions.registrationRequiresEmail()).toEqual({ type: Types.REGISTRATION_REQUIRES_EMAIL });
|
||||
expect(Actions.registrationRequiresEmail()).toEqual({ type: Types.REGISTRATION_REQUIRES_EMAIL, payload: undefined });
|
||||
});
|
||||
|
||||
it('registrationSuccess', () => {
|
||||
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCESS });
|
||||
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCESS, payload: undefined });
|
||||
});
|
||||
|
||||
it('registrationFailed', () => {
|
||||
expect(Actions.registrationFailed('err', 999)).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: 999 });
|
||||
const action = Actions.registrationFailed({ reason: 'err', endTime: 999 });
|
||||
expect(action.payload).toEqual({ reason: 'err', endTime: 999 });
|
||||
});
|
||||
|
||||
it('registrationFailed without endTime', () => {
|
||||
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: undefined });
|
||||
const action = Actions.registrationFailed({ reason: 'err' });
|
||||
expect(action.payload).toEqual({ reason: 'err' });
|
||||
});
|
||||
|
||||
it('registrationEmailError', () => {
|
||||
expect(Actions.registrationEmailError('bad email')).toEqual({ type: Types.REGISTRATION_EMAIL_ERROR, error: 'bad email' });
|
||||
const action = Actions.registrationEmailError({ error: 'bad email' });
|
||||
expect(action.payload).toEqual({ error: 'bad email' });
|
||||
});
|
||||
|
||||
it('registrationPasswordError', () => {
|
||||
expect(Actions.registrationPasswordError('bad pw')).toEqual({ type: Types.REGISTRATION_PASSWORD_ERROR, error: 'bad pw' });
|
||||
const action = Actions.registrationPasswordError({ error: 'bad pw' });
|
||||
expect(action.payload).toEqual({ error: 'bad pw' });
|
||||
});
|
||||
|
||||
it('registrationUserNameError', () => {
|
||||
expect(Actions.registrationUserNameError('bad name')).toEqual({ type: Types.REGISTRATION_USERNAME_ERROR, error: 'bad name' });
|
||||
const action = Actions.registrationUserNameError({ error: 'bad name' });
|
||||
expect(action.payload).toEqual({ error: 'bad name' });
|
||||
});
|
||||
|
||||
it('accountAwaitingActivation', () => {
|
||||
const options = makePendingActivationContext();
|
||||
expect(Actions.accountAwaitingActivation(options)).toEqual({ type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
|
||||
expect(Actions.accountAwaitingActivation({ options })).toEqual({ type: Types.ACCOUNT_AWAITING_ACTIVATION, payload: { options } });
|
||||
});
|
||||
|
||||
it('accountActivationSuccess', () => {
|
||||
expect(Actions.accountActivationSuccess()).toEqual({ type: Types.ACCOUNT_ACTIVATION_SUCCESS });
|
||||
expect(Actions.accountActivationSuccess()).toEqual({ type: Types.ACCOUNT_ACTIVATION_SUCCESS, payload: undefined });
|
||||
});
|
||||
|
||||
it('accountActivationFailed', () => {
|
||||
expect(Actions.accountActivationFailed()).toEqual({ type: Types.ACCOUNT_ACTIVATION_FAILED });
|
||||
expect(Actions.accountActivationFailed()).toEqual({ type: Types.ACCOUNT_ACTIVATION_FAILED, payload: undefined });
|
||||
});
|
||||
|
||||
it('resetPassword', () => {
|
||||
expect(Actions.resetPassword()).toEqual({ type: Types.RESET_PASSWORD_REQUESTED });
|
||||
expect(Actions.resetPassword()).toEqual({ type: Types.RESET_PASSWORD_REQUESTED, payload: undefined });
|
||||
});
|
||||
|
||||
it('resetPasswordFailed', () => {
|
||||
expect(Actions.resetPasswordFailed()).toEqual({ type: Types.RESET_PASSWORD_FAILED });
|
||||
expect(Actions.resetPasswordFailed()).toEqual({ type: Types.RESET_PASSWORD_FAILED, payload: undefined });
|
||||
});
|
||||
|
||||
it('resetPasswordChallenge', () => {
|
||||
expect(Actions.resetPasswordChallenge()).toEqual({ type: Types.RESET_PASSWORD_CHALLENGE });
|
||||
expect(Actions.resetPasswordChallenge()).toEqual({ type: Types.RESET_PASSWORD_CHALLENGE, payload: undefined });
|
||||
});
|
||||
|
||||
it('resetPasswordSuccess', () => {
|
||||
expect(Actions.resetPasswordSuccess()).toEqual({ type: Types.RESET_PASSWORD_SUCCESS });
|
||||
expect(Actions.resetPasswordSuccess()).toEqual({ type: Types.RESET_PASSWORD_SUCCESS, payload: undefined });
|
||||
});
|
||||
|
||||
it('adjustMod', () => {
|
||||
expect(Actions.adjustMod('Dan', true, false)).toEqual({
|
||||
expect(Actions.adjustMod({ userName: 'Dan', shouldBeMod: true, shouldBeJudge: false })).toEqual({
|
||||
type: Types.ADJUST_MOD,
|
||||
userName: 'Dan',
|
||||
shouldBeMod: true,
|
||||
shouldBeJudge: false,
|
||||
payload: { userName: 'Dan', shouldBeMod: true, shouldBeJudge: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('reloadConfig', () => {
|
||||
expect(Actions.reloadConfig()).toEqual({ type: Types.RELOAD_CONFIG });
|
||||
expect(Actions.reloadConfig()).toEqual({ type: Types.RELOAD_CONFIG, payload: undefined });
|
||||
});
|
||||
|
||||
it('shutdownServer', () => {
|
||||
expect(Actions.shutdownServer()).toEqual({ type: Types.SHUTDOWN_SERVER });
|
||||
expect(Actions.shutdownServer()).toEqual({ type: Types.SHUTDOWN_SERVER, payload: undefined });
|
||||
});
|
||||
|
||||
it('updateServerMessage', () => {
|
||||
expect(Actions.updateServerMessage()).toEqual({ type: Types.UPDATE_SERVER_MESSAGE });
|
||||
expect(Actions.updateServerMessage()).toEqual({ type: Types.UPDATE_SERVER_MESSAGE, payload: undefined });
|
||||
});
|
||||
|
||||
it('accountPasswordChange', () => {
|
||||
expect(Actions.accountPasswordChange()).toEqual({ type: Types.ACCOUNT_PASSWORD_CHANGE });
|
||||
expect(Actions.accountPasswordChange()).toEqual({ type: Types.ACCOUNT_PASSWORD_CHANGE, payload: undefined });
|
||||
});
|
||||
|
||||
it('accountEditChanged', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.accountEditChanged(user)).toEqual({ type: Types.ACCOUNT_EDIT_CHANGED, user });
|
||||
expect(Actions.accountEditChanged({ user })).toEqual({ type: Types.ACCOUNT_EDIT_CHANGED, payload: { user } });
|
||||
});
|
||||
|
||||
it('accountImageChanged', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.accountImageChanged(user)).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, user });
|
||||
expect(Actions.accountImageChanged({ user })).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, payload: { user } });
|
||||
});
|
||||
|
||||
it('getUserInfo', () => {
|
||||
const userInfo = makeUser({ name: 'Frank' });
|
||||
expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo });
|
||||
expect(Actions.getUserInfo({ userInfo })).toEqual({ type: Types.GET_USER_INFO, payload: { userInfo } });
|
||||
});
|
||||
|
||||
it('notifyUser', () => {
|
||||
const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
|
||||
expect(Actions.notifyUser({ notification })).toEqual({ type: Types.NOTIFY_USER, payload: { notification } });
|
||||
});
|
||||
|
||||
it('serverShutdown', () => {
|
||||
const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
|
||||
expect(Actions.serverShutdown({ data })).toEqual({ type: Types.SERVER_SHUTDOWN, payload: { data } });
|
||||
});
|
||||
|
||||
it('userMessage', () => {
|
||||
const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
|
||||
expect(Actions.userMessage({ messageData })).toEqual({ type: Types.USER_MESSAGE, payload: { messageData } });
|
||||
});
|
||||
|
||||
it('addToList', () => {
|
||||
expect(Actions.addToList('buddyList', 'Grace')).toEqual({
|
||||
expect(Actions.addToList({ list: 'buddyList', userName: 'Grace' })).toEqual({
|
||||
type: Types.ADD_TO_LIST,
|
||||
list: 'buddyList',
|
||||
userName: 'Grace',
|
||||
payload: { list: 'buddyList', userName: 'Grace' },
|
||||
});
|
||||
});
|
||||
|
||||
it('removeFromList', () => {
|
||||
expect(Actions.removeFromList('buddyList', 'Hank')).toEqual({
|
||||
expect(Actions.removeFromList({ list: 'buddyList', userName: 'Hank' })).toEqual({
|
||||
type: Types.REMOVE_FROM_LIST,
|
||||
list: 'buddyList',
|
||||
userName: 'Hank',
|
||||
payload: { list: 'buddyList', userName: 'Hank' },
|
||||
});
|
||||
});
|
||||
|
||||
it('banFromServer', () => {
|
||||
expect(Actions.banFromServer('Ira')).toEqual({ type: Types.BAN_FROM_SERVER, userName: 'Ira' });
|
||||
expect(Actions.banFromServer({ userName: 'Ira' })).toEqual({ type: Types.BAN_FROM_SERVER, payload: { userName: 'Ira' } });
|
||||
});
|
||||
|
||||
it('banHistory', () => {
|
||||
const history = [makeBanHistoryItem()];
|
||||
expect(Actions.banHistory('Ira', history)).toEqual({ type: Types.BAN_HISTORY, userName: 'Ira', banHistory: history });
|
||||
const action = Actions.banHistory({ userName: 'Ira', banHistory: history });
|
||||
expect(action.payload).toEqual({ userName: 'Ira', banHistory: history });
|
||||
});
|
||||
|
||||
it('warnHistory', () => {
|
||||
const history = [makeWarnHistoryItem()];
|
||||
expect(Actions.warnHistory('Jack', history)).toEqual({ type: Types.WARN_HISTORY, userName: 'Jack', warnHistory: history });
|
||||
const action = Actions.warnHistory({ userName: 'Jack', warnHistory: history });
|
||||
expect(action.payload).toEqual({ userName: 'Jack', warnHistory: history });
|
||||
});
|
||||
|
||||
it('warnListOptions', () => {
|
||||
const list = [makeWarnListItem()];
|
||||
expect(Actions.warnListOptions(list)).toEqual({ type: Types.WARN_LIST_OPTIONS, warnList: list });
|
||||
expect(Actions.warnListOptions({ warnList: list })).toEqual({ type: Types.WARN_LIST_OPTIONS, payload: { warnList: list } });
|
||||
});
|
||||
|
||||
it('warnUser', () => {
|
||||
expect(Actions.warnUser('Kelly')).toEqual({ type: Types.WARN_USER, userName: 'Kelly' });
|
||||
expect(Actions.warnUser({ userName: 'Kelly' })).toEqual({ type: Types.WARN_USER, payload: { userName: 'Kelly' } });
|
||||
});
|
||||
|
||||
it('grantReplayAccess', () => {
|
||||
expect(Actions.grantReplayAccess(7, 'Moe')).toEqual({
|
||||
expect(Actions.grantReplayAccess({ replayId: 7, moderatorName: 'Moe' })).toEqual({
|
||||
type: Types.GRANT_REPLAY_ACCESS,
|
||||
replayId: 7,
|
||||
moderatorName: 'Moe',
|
||||
payload: { replayId: 7, moderatorName: 'Moe' },
|
||||
});
|
||||
});
|
||||
|
||||
it('forceActivateUser', () => {
|
||||
expect(Actions.forceActivateUser('Ned', 'Moe')).toEqual({
|
||||
expect(Actions.forceActivateUser({ usernameToActivate: 'Ned', moderatorName: 'Moe' })).toEqual({
|
||||
type: Types.FORCE_ACTIVATE_USER,
|
||||
usernameToActivate: 'Ned',
|
||||
moderatorName: 'Moe',
|
||||
payload: { usernameToActivate: 'Ned', moderatorName: 'Moe' },
|
||||
});
|
||||
});
|
||||
|
||||
it('getAdminNotes', () => {
|
||||
expect(Actions.getAdminNotes('Ned', 'some notes')).toEqual({
|
||||
expect(Actions.getAdminNotes({ userName: 'Ned', notes: 'some notes' })).toEqual({
|
||||
type: Types.GET_ADMIN_NOTES,
|
||||
userName: 'Ned',
|
||||
notes: 'some notes',
|
||||
payload: { userName: 'Ned', notes: 'some notes' },
|
||||
});
|
||||
});
|
||||
|
||||
it('updateAdminNotes', () => {
|
||||
expect(Actions.updateAdminNotes('Ned', 'updated notes')).toEqual({
|
||||
expect(Actions.updateAdminNotes({ userName: 'Ned', notes: 'updated notes' })).toEqual({
|
||||
type: Types.UPDATE_ADMIN_NOTES,
|
||||
userName: 'Ned',
|
||||
notes: 'updated notes',
|
||||
payload: { userName: 'Ned', notes: 'updated notes' },
|
||||
});
|
||||
});
|
||||
|
||||
it('replayList', () => {
|
||||
const list = [makeReplayMatch()];
|
||||
expect(Actions.replayList(list)).toEqual({ type: Types.REPLAY_LIST, matchList: list });
|
||||
expect(Actions.replayList({ matchList: list })).toEqual({ type: Types.REPLAY_LIST, payload: { matchList: list } });
|
||||
});
|
||||
|
||||
it('replayAdded', () => {
|
||||
const match = makeReplayMatch();
|
||||
expect(Actions.replayAdded(match)).toEqual({ type: Types.REPLAY_ADDED, matchInfo: match });
|
||||
expect(Actions.replayAdded({ matchInfo: match })).toEqual({ type: Types.REPLAY_ADDED, payload: { matchInfo: match } });
|
||||
});
|
||||
|
||||
it('replayModifyMatch', () => {
|
||||
expect(Actions.replayModifyMatch(5, true)).toEqual({
|
||||
expect(Actions.replayModifyMatch({ gameId: 5, doNotHide: true })).toEqual({
|
||||
type: Types.REPLAY_MODIFY_MATCH,
|
||||
gameId: 5,
|
||||
doNotHide: true,
|
||||
payload: { gameId: 5, doNotHide: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('replayDeleteMatch', () => {
|
||||
expect(Actions.replayDeleteMatch(5)).toEqual({ type: Types.REPLAY_DELETE_MATCH, gameId: 5 });
|
||||
expect(Actions.replayDeleteMatch({ gameId: 5 })).toEqual({ type: Types.REPLAY_DELETE_MATCH, payload: { gameId: 5 } });
|
||||
});
|
||||
|
||||
it('backendDecks', () => {
|
||||
const deckList = makeDeckList();
|
||||
expect(Actions.backendDecks(deckList)).toEqual({ type: Types.BACKEND_DECKS, deckList });
|
||||
expect(Actions.backendDecks({ deckList })).toEqual({ type: Types.BACKEND_DECKS, payload: { deckList } });
|
||||
});
|
||||
|
||||
it('deckNewDir', () => {
|
||||
expect(Actions.deckNewDir('a/b', 'newFolder')).toEqual({
|
||||
expect(Actions.deckNewDir({ path: 'a/b', dirName: 'newFolder' })).toEqual({
|
||||
type: Types.DECK_NEW_DIR,
|
||||
path: 'a/b',
|
||||
dirName: 'newFolder',
|
||||
payload: { path: 'a/b', dirName: 'newFolder' },
|
||||
});
|
||||
});
|
||||
|
||||
it('deckDelDir', () => {
|
||||
expect(Actions.deckDelDir('a/b')).toEqual({ type: Types.DECK_DEL_DIR, path: 'a/b' });
|
||||
expect(Actions.deckDelDir({ path: 'a/b' })).toEqual({ type: Types.DECK_DEL_DIR, payload: { path: 'a/b' } });
|
||||
});
|
||||
|
||||
it('deckUpload', () => {
|
||||
const treeItem = makeDeckTreeItem();
|
||||
expect(Actions.deckUpload('a/b', treeItem)).toEqual({
|
||||
expect(Actions.deckUpload({ path: 'a/b', treeItem })).toEqual({
|
||||
type: Types.DECK_UPLOAD,
|
||||
path: 'a/b',
|
||||
treeItem,
|
||||
payload: { path: 'a/b', treeItem },
|
||||
});
|
||||
});
|
||||
|
||||
it('deckDelete', () => {
|
||||
expect(Actions.deckDelete(42)).toEqual({ type: Types.DECK_DELETE, deckId: 42 });
|
||||
expect(Actions.deckDelete({ deckId: 42 })).toEqual({ type: Types.DECK_DELETE, payload: { deckId: 42 } });
|
||||
});
|
||||
|
||||
it('gamesOfUser', () => {
|
||||
const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] });
|
||||
expect(Actions.gamesOfUser('alice', response)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', response });
|
||||
const action = Actions.gamesOfUser({ userName: 'alice', response });
|
||||
expect(action.payload).toEqual({ userName: 'alice', response });
|
||||
});
|
||||
|
||||
it('clearRegistrationErrors', () => {
|
||||
expect(Actions.clearRegistrationErrors()).toEqual({ type: Types.CLEAR_REGISTRATION_ERRORS });
|
||||
expect(Actions.clearRegistrationErrors()).toEqual({ type: Types.CLEAR_REGISTRATION_ERRORS, payload: undefined });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,245 +1,5 @@
|
|||
import { Data, Enriched } from '@app/types';
|
||||
import { ServerStateStatus } from './server.interfaces';
|
||||
import { Types } from './server.types';
|
||||
import { serverSlice } from './server.reducer';
|
||||
|
||||
export const Actions = {
|
||||
initialized: () => ({
|
||||
type: Types.INITIALIZED
|
||||
}),
|
||||
clearStore: () => ({
|
||||
type: Types.CLEAR_STORE
|
||||
}),
|
||||
connectionAttempted: () => ({
|
||||
type: Types.CONNECTION_ATTEMPTED
|
||||
}),
|
||||
loginSuccessful: (options: Enriched.LoginSuccessContext) => ({
|
||||
type: Types.LOGIN_SUCCESSFUL,
|
||||
options
|
||||
}),
|
||||
loginFailed: () => ({
|
||||
type: Types.LOGIN_FAILED,
|
||||
}),
|
||||
connectionFailed: () => ({
|
||||
type: Types.CONNECTION_FAILED,
|
||||
}),
|
||||
testConnectionSuccessful: () => ({
|
||||
type: Types.TEST_CONNECTION_SUCCESSFUL,
|
||||
}),
|
||||
testConnectionFailed: () => ({
|
||||
type: Types.TEST_CONNECTION_FAILED,
|
||||
}),
|
||||
serverMessage: (message: string) => ({
|
||||
type: Types.SERVER_MESSAGE,
|
||||
message
|
||||
}),
|
||||
updateBuddyList: (buddyList: Data.ServerInfo_User[]) => ({
|
||||
type: Types.UPDATE_BUDDY_LIST,
|
||||
buddyList
|
||||
}),
|
||||
addToBuddyList: (user: Data.ServerInfo_User) => ({
|
||||
type: Types.ADD_TO_BUDDY_LIST,
|
||||
user
|
||||
}),
|
||||
removeFromBuddyList: (userName: string) => ({
|
||||
type: Types.REMOVE_FROM_BUDDY_LIST,
|
||||
userName
|
||||
}),
|
||||
updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => ({
|
||||
type: Types.UPDATE_IGNORE_LIST,
|
||||
ignoreList
|
||||
}),
|
||||
addToIgnoreList: (user: Data.ServerInfo_User) => ({
|
||||
type: Types.ADD_TO_IGNORE_LIST,
|
||||
user
|
||||
}),
|
||||
removeFromIgnoreList: (userName: string) => ({
|
||||
type: Types.REMOVE_FROM_IGNORE_LIST,
|
||||
userName
|
||||
}),
|
||||
updateInfo: (info: { name: string; version: string }) => ({
|
||||
type: Types.UPDATE_INFO,
|
||||
info
|
||||
}),
|
||||
updateStatus: (status: Pick<ServerStateStatus, 'state' | 'description'>) => ({
|
||||
type: Types.UPDATE_STATUS,
|
||||
status
|
||||
}),
|
||||
updateUser: (user: Data.ServerInfo_User) => ({
|
||||
type: Types.UPDATE_USER,
|
||||
user
|
||||
}),
|
||||
updateUsers: (users: Data.ServerInfo_User[]) => ({
|
||||
type: Types.UPDATE_USERS,
|
||||
users
|
||||
}),
|
||||
userJoined: (user: Data.ServerInfo_User) => ({
|
||||
type: Types.USER_JOINED,
|
||||
user
|
||||
}),
|
||||
userLeft: (name: string) => ({
|
||||
type: Types.USER_LEFT,
|
||||
name
|
||||
}),
|
||||
viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => ({
|
||||
type: Types.VIEW_LOGS,
|
||||
logs
|
||||
}),
|
||||
clearLogs: () => ({
|
||||
type: Types.CLEAR_LOGS,
|
||||
}),
|
||||
registrationRequiresEmail: () => ({
|
||||
type: Types.REGISTRATION_REQUIRES_EMAIL,
|
||||
}),
|
||||
registrationSuccess: () => ({
|
||||
type: Types.REGISTRATION_SUCCESS,
|
||||
}),
|
||||
registrationFailed: (reason: string, endTime?: number) => ({
|
||||
type: Types.REGISTRATION_FAILED,
|
||||
reason,
|
||||
endTime,
|
||||
}),
|
||||
registrationEmailError: (error: string) => ({
|
||||
type: Types.REGISTRATION_EMAIL_ERROR,
|
||||
error
|
||||
}),
|
||||
registrationPasswordError: (error: string) => ({
|
||||
type: Types.REGISTRATION_PASSWORD_ERROR,
|
||||
error
|
||||
}),
|
||||
registrationUserNameError: (error: string) => ({
|
||||
type: Types.REGISTRATION_USERNAME_ERROR,
|
||||
error
|
||||
}),
|
||||
clearRegistrationErrors: () => ({
|
||||
type: Types.CLEAR_REGISTRATION_ERRORS,
|
||||
}),
|
||||
accountAwaitingActivation: (options: Enriched.PendingActivationContext) => ({
|
||||
type: Types.ACCOUNT_AWAITING_ACTIVATION,
|
||||
options
|
||||
}),
|
||||
accountActivationSuccess: () => ({
|
||||
type: Types.ACCOUNT_ACTIVATION_SUCCESS,
|
||||
}),
|
||||
accountActivationFailed: () => ({
|
||||
type: Types.ACCOUNT_ACTIVATION_FAILED,
|
||||
}),
|
||||
resetPassword: () => ({
|
||||
type: Types.RESET_PASSWORD_REQUESTED,
|
||||
}),
|
||||
resetPasswordFailed: () => ({
|
||||
type: Types.RESET_PASSWORD_FAILED,
|
||||
}),
|
||||
resetPasswordChallenge: () => ({
|
||||
type: Types.RESET_PASSWORD_CHALLENGE,
|
||||
}),
|
||||
resetPasswordSuccess: () => ({
|
||||
type: Types.RESET_PASSWORD_SUCCESS,
|
||||
}),
|
||||
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => ({
|
||||
type: Types.ADJUST_MOD,
|
||||
userName,
|
||||
shouldBeMod,
|
||||
shouldBeJudge,
|
||||
}),
|
||||
reloadConfig: () => ({
|
||||
type: Types.RELOAD_CONFIG,
|
||||
}),
|
||||
shutdownServer: () => ({
|
||||
type: Types.SHUTDOWN_SERVER,
|
||||
}),
|
||||
updateServerMessage: () => ({
|
||||
type: Types.UPDATE_SERVER_MESSAGE,
|
||||
}),
|
||||
accountPasswordChange: () => ({
|
||||
type: Types.ACCOUNT_PASSWORD_CHANGE,
|
||||
}),
|
||||
accountEditChanged: (user: Partial<Data.ServerInfo_User>) => ({
|
||||
type: Types.ACCOUNT_EDIT_CHANGED,
|
||||
user,
|
||||
}),
|
||||
accountImageChanged: (user: Partial<Data.ServerInfo_User>) => ({
|
||||
type: Types.ACCOUNT_IMAGE_CHANGED,
|
||||
user,
|
||||
}),
|
||||
getUserInfo: (userInfo: Data.ServerInfo_User) => ({
|
||||
type: Types.GET_USER_INFO,
|
||||
userInfo,
|
||||
}),
|
||||
notifyUser: (notification: Data.Event_NotifyUser) => ({
|
||||
type: Types.NOTIFY_USER,
|
||||
notification,
|
||||
}),
|
||||
serverShutdown: (data: Data.Event_ServerShutdown) => ({
|
||||
type: Types.SERVER_SHUTDOWN,
|
||||
data,
|
||||
}),
|
||||
userMessage: (messageData: Data.Event_UserMessage) => ({
|
||||
type: Types.USER_MESSAGE,
|
||||
messageData,
|
||||
}),
|
||||
addToList: (list: string, userName: string) => ({
|
||||
type: Types.ADD_TO_LIST,
|
||||
list,
|
||||
userName,
|
||||
}),
|
||||
removeFromList: (list: string, userName: string) => ({
|
||||
type: Types.REMOVE_FROM_LIST,
|
||||
list,
|
||||
userName,
|
||||
}),
|
||||
banFromServer: (userName: string) => ({
|
||||
type: Types.BAN_FROM_SERVER,
|
||||
userName,
|
||||
}),
|
||||
banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => ({
|
||||
type: Types.BAN_HISTORY,
|
||||
userName,
|
||||
banHistory,
|
||||
}),
|
||||
warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => ({
|
||||
type: Types.WARN_HISTORY,
|
||||
userName,
|
||||
warnHistory,
|
||||
}),
|
||||
warnListOptions: (warnList: Data.Response_WarnList[]) => ({
|
||||
type: Types.WARN_LIST_OPTIONS,
|
||||
warnList,
|
||||
}),
|
||||
warnUser: (userName: string) => ({
|
||||
type: Types.WARN_USER,
|
||||
userName,
|
||||
}),
|
||||
grantReplayAccess: (replayId: number, moderatorName: string) => ({
|
||||
type: Types.GRANT_REPLAY_ACCESS,
|
||||
replayId,
|
||||
moderatorName,
|
||||
}),
|
||||
forceActivateUser: (usernameToActivate: string, moderatorName: string) => ({
|
||||
type: Types.FORCE_ACTIVATE_USER,
|
||||
usernameToActivate,
|
||||
moderatorName,
|
||||
}),
|
||||
getAdminNotes: (userName: string, notes: string) => ({
|
||||
type: Types.GET_ADMIN_NOTES,
|
||||
userName,
|
||||
notes,
|
||||
}),
|
||||
updateAdminNotes: (userName: string, notes: string) => ({
|
||||
type: Types.UPDATE_ADMIN_NOTES,
|
||||
userName,
|
||||
notes,
|
||||
}),
|
||||
replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }),
|
||||
replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }),
|
||||
replayModifyMatch: (gameId: number, doNotHide: boolean) => ({ type: Types.REPLAY_MODIFY_MATCH, gameId, doNotHide }),
|
||||
replayDeleteMatch: (gameId: number) => ({ type: Types.REPLAY_DELETE_MATCH, gameId }),
|
||||
backendDecks: (deckList: Data.Response_DeckList) => ({ type: Types.BACKEND_DECKS, deckList }),
|
||||
deckNewDir: (path: string, dirName: string) => ({ type: Types.DECK_NEW_DIR, path, dirName }),
|
||||
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
||||
deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
||||
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
||||
gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) =>
|
||||
({ type: Types.GAMES_OF_USER, userName, response }),
|
||||
}
|
||||
export const Actions = serverSlice.actions;
|
||||
|
||||
export type ServerAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ describe('Dispatch', () => {
|
|||
it('loginSuccessful dispatches Actions.loginSuccessful()', () => {
|
||||
const options = makeLoginSuccessContext();
|
||||
Dispatch.loginSuccessful(options);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.loginSuccessful(options));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.loginSuccessful({ options }));
|
||||
});
|
||||
|
||||
it('loginFailed dispatches Actions.loginFailed()', () => {
|
||||
|
|
@ -69,74 +69,74 @@ describe('Dispatch', () => {
|
|||
it('updateBuddyList dispatches Actions.updateBuddyList()', () => {
|
||||
const list = [makeUser()];
|
||||
Dispatch.updateBuddyList(list);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateBuddyList(list));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateBuddyList({ buddyList: list }));
|
||||
});
|
||||
|
||||
it('addToBuddyList dispatches Actions.addToBuddyList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToBuddyList(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToBuddyList(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToBuddyList({ user }));
|
||||
});
|
||||
|
||||
it('removeFromBuddyList dispatches Actions.removeFromBuddyList()', () => {
|
||||
Dispatch.removeFromBuddyList('Alice');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromBuddyList('Alice'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromBuddyList({ userName: 'Alice' }));
|
||||
});
|
||||
|
||||
it('updateIgnoreList dispatches Actions.updateIgnoreList()', () => {
|
||||
const list = [makeUser()];
|
||||
Dispatch.updateIgnoreList(list);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateIgnoreList(list));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateIgnoreList({ ignoreList: list }));
|
||||
});
|
||||
|
||||
it('addToIgnoreList dispatches Actions.addToIgnoreList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToIgnoreList(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToIgnoreList(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToIgnoreList({ user }));
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList dispatches Actions.removeFromIgnoreList()', () => {
|
||||
Dispatch.removeFromIgnoreList('Bob');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromIgnoreList('Bob'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromIgnoreList({ userName: 'Bob' }));
|
||||
});
|
||||
|
||||
it('updateInfo dispatches Actions.updateInfo({ name, version })', () => {
|
||||
it('updateInfo dispatches Actions.updateInfo({ info: { name, version } })', () => {
|
||||
Dispatch.updateInfo('Servatrice', '2.9');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateInfo({ name: 'Servatrice', version: '2.9' }));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateInfo({ info: { name: 'Servatrice', version: '2.9' } }));
|
||||
});
|
||||
|
||||
it('updateStatus dispatches Actions.updateStatus({ state, description })', () => {
|
||||
it('updateStatus dispatches Actions.updateStatus({ status: { state, description } })', () => {
|
||||
Dispatch.updateStatus(App.StatusEnum.CONNECTED, 'ok');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: App.StatusEnum.CONNECTED, description: 'ok' }));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateStatus({ status: { state: App.StatusEnum.CONNECTED, description: 'ok' } }));
|
||||
});
|
||||
|
||||
it('updateUser dispatches Actions.updateUser()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.updateUser(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateUser(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateUser({ user }));
|
||||
});
|
||||
|
||||
it('updateUsers dispatches Actions.updateUsers()', () => {
|
||||
const users = [makeUser()];
|
||||
Dispatch.updateUsers(users);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateUsers(users));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateUsers({ users }));
|
||||
});
|
||||
|
||||
it('userJoined dispatches Actions.userJoined()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.userJoined(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userJoined(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userJoined({ user }));
|
||||
});
|
||||
|
||||
it('userLeft dispatches Actions.userLeft()', () => {
|
||||
Dispatch.userLeft('Carol');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userLeft('Carol'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userLeft({ name: 'Carol' }));
|
||||
});
|
||||
|
||||
it('viewLogs dispatches Actions.viewLogs()', () => {
|
||||
const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })];
|
||||
Dispatch.viewLogs(logs);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.viewLogs({ logs }));
|
||||
});
|
||||
|
||||
it('clearLogs dispatches Actions.clearLogs()', () => {
|
||||
|
|
@ -146,7 +146,7 @@ describe('Dispatch', () => {
|
|||
|
||||
it('serverMessage dispatches Actions.serverMessage()', () => {
|
||||
Dispatch.serverMessage('Welcome!');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.serverMessage('Welcome!'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.serverMessage({ message: 'Welcome!' }));
|
||||
});
|
||||
|
||||
it('registrationRequiresEmail dispatches correctly', () => {
|
||||
|
|
@ -161,33 +161,33 @@ describe('Dispatch', () => {
|
|||
|
||||
it('registrationFailed passes reason and endTime to action', () => {
|
||||
Dispatch.registrationFailed('reason', 999);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationFailed('reason', 999));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationFailed({ reason: 'reason', endTime: 999 }));
|
||||
});
|
||||
|
||||
it('registrationFailed passes reason only when no endTime', () => {
|
||||
Dispatch.registrationFailed('plain reason');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationFailed('plain reason', undefined));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationFailed({ reason: 'plain reason', endTime: undefined }));
|
||||
});
|
||||
|
||||
it('registrationEmailError dispatches correctly', () => {
|
||||
Dispatch.registrationEmailError('bad');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationEmailError('bad'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationEmailError({ error: 'bad' }));
|
||||
});
|
||||
|
||||
it('registrationPasswordError dispatches correctly', () => {
|
||||
Dispatch.registrationPasswordError('weak');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationPasswordError('weak'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationPasswordError({ error: 'weak' }));
|
||||
});
|
||||
|
||||
it('registrationUserNameError dispatches correctly', () => {
|
||||
Dispatch.registrationUserNameError('taken');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationUserNameError('taken'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.registrationUserNameError({ error: 'taken' }));
|
||||
});
|
||||
|
||||
it('accountAwaitingActivation dispatches correctly', () => {
|
||||
const options = makePendingActivationContext();
|
||||
Dispatch.accountAwaitingActivation(options);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountAwaitingActivation(options));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountAwaitingActivation({ options }));
|
||||
});
|
||||
|
||||
it('accountActivationSuccess dispatches correctly', () => {
|
||||
|
|
@ -222,7 +222,7 @@ describe('Dispatch', () => {
|
|||
|
||||
it('adjustMod dispatches Actions.adjustMod()', () => {
|
||||
Dispatch.adjustMod('Dan', true, false);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.adjustMod('Dan', true, false));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.adjustMod({ userName: 'Dan', shouldBeMod: true, shouldBeJudge: false }));
|
||||
});
|
||||
|
||||
it('reloadConfig dispatches correctly', () => {
|
||||
|
|
@ -248,150 +248,150 @@ describe('Dispatch', () => {
|
|||
it('accountEditChanged dispatches correctly', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.accountEditChanged(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountEditChanged(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountEditChanged({ user }));
|
||||
});
|
||||
|
||||
it('accountImageChanged dispatches correctly', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.accountImageChanged(user);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountImageChanged(user));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.accountImageChanged({ user }));
|
||||
});
|
||||
|
||||
it('getUserInfo dispatches correctly', () => {
|
||||
const userInfo = makeUser({ name: 'Frank' });
|
||||
Dispatch.getUserInfo(userInfo);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.getUserInfo(userInfo));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.getUserInfo({ userInfo }));
|
||||
});
|
||||
|
||||
it('notifyUser dispatches correctly', () => {
|
||||
const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||
Dispatch.notifyUser(notification);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.notifyUser({ notification }));
|
||||
});
|
||||
|
||||
it('serverShutdown dispatches correctly', () => {
|
||||
const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||
Dispatch.serverShutdown(data);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.serverShutdown({ data }));
|
||||
});
|
||||
|
||||
it('userMessage dispatches correctly', () => {
|
||||
const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||
Dispatch.userMessage(messageData);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.userMessage({ messageData }));
|
||||
});
|
||||
|
||||
it('addToList dispatches correctly', () => {
|
||||
Dispatch.addToList('buddyList', 'Grace');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToList('buddyList', 'Grace'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.addToList({ list: 'buddyList', userName: 'Grace' }));
|
||||
});
|
||||
|
||||
it('removeFromList dispatches correctly', () => {
|
||||
Dispatch.removeFromList('buddyList', 'Hank');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromList('buddyList', 'Hank'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.removeFromList({ list: 'buddyList', userName: 'Hank' }));
|
||||
});
|
||||
|
||||
it('banFromServer dispatches correctly', () => {
|
||||
Dispatch.banFromServer('Ira');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.banFromServer('Ira'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.banFromServer({ userName: 'Ira' }));
|
||||
});
|
||||
|
||||
it('banHistory dispatches correctly', () => {
|
||||
const history = [makeBanHistoryItem()];
|
||||
Dispatch.banHistory('Ira', history);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.banHistory('Ira', history));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.banHistory({ userName: 'Ira', banHistory: history }));
|
||||
});
|
||||
|
||||
it('warnHistory dispatches correctly', () => {
|
||||
const history = [makeWarnHistoryItem()];
|
||||
Dispatch.warnHistory('Jack', history);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnHistory('Jack', history));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnHistory({ userName: 'Jack', warnHistory: history }));
|
||||
});
|
||||
|
||||
it('warnListOptions dispatches correctly', () => {
|
||||
const list = [makeWarnListItem()];
|
||||
Dispatch.warnListOptions(list);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnListOptions(list));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnListOptions({ warnList: list }));
|
||||
});
|
||||
|
||||
it('warnUser dispatches correctly', () => {
|
||||
Dispatch.warnUser('Kelly');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnUser('Kelly'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.warnUser({ userName: 'Kelly' }));
|
||||
});
|
||||
|
||||
it('grantReplayAccess dispatches correctly', () => {
|
||||
Dispatch.grantReplayAccess(7, 'Moe');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.grantReplayAccess(7, 'Moe'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.grantReplayAccess({ replayId: 7, moderatorName: 'Moe' }));
|
||||
});
|
||||
|
||||
it('forceActivateUser dispatches correctly', () => {
|
||||
Dispatch.forceActivateUser('Ned', 'Moe');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.forceActivateUser('Ned', 'Moe'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.forceActivateUser({ usernameToActivate: 'Ned', moderatorName: 'Moe' }));
|
||||
});
|
||||
|
||||
it('getAdminNotes dispatches correctly', () => {
|
||||
Dispatch.getAdminNotes('Ned', 'notes');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.getAdminNotes('Ned', 'notes'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.getAdminNotes({ userName: 'Ned', notes: 'notes' }));
|
||||
});
|
||||
|
||||
it('updateAdminNotes dispatches correctly', () => {
|
||||
Dispatch.updateAdminNotes('Ned', 'updated');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateAdminNotes('Ned', 'updated'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.updateAdminNotes({ userName: 'Ned', notes: 'updated' }));
|
||||
});
|
||||
|
||||
it('replayList dispatches correctly', () => {
|
||||
const list = [makeReplayMatch()];
|
||||
Dispatch.replayList(list);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayList(list));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayList({ matchList: list }));
|
||||
});
|
||||
|
||||
it('replayAdded dispatches correctly', () => {
|
||||
const match = makeReplayMatch();
|
||||
Dispatch.replayAdded(match);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayAdded(match));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayAdded({ matchInfo: match }));
|
||||
});
|
||||
|
||||
it('replayModifyMatch dispatches correctly', () => {
|
||||
Dispatch.replayModifyMatch(5, true);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayModifyMatch(5, true));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayModifyMatch({ gameId: 5, doNotHide: true }));
|
||||
});
|
||||
|
||||
it('replayDeleteMatch dispatches correctly', () => {
|
||||
Dispatch.replayDeleteMatch(5);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayDeleteMatch(5));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.replayDeleteMatch({ gameId: 5 }));
|
||||
});
|
||||
|
||||
it('backendDecks dispatches correctly', () => {
|
||||
const deckList = makeDeckList();
|
||||
Dispatch.backendDecks(deckList);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.backendDecks(deckList));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.backendDecks({ deckList }));
|
||||
});
|
||||
|
||||
it('deckNewDir dispatches correctly', () => {
|
||||
Dispatch.deckNewDir('a/b', 'newFolder');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckNewDir('a/b', 'newFolder'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckNewDir({ path: 'a/b', dirName: 'newFolder' }));
|
||||
});
|
||||
|
||||
it('deckDelDir dispatches correctly', () => {
|
||||
Dispatch.deckDelDir('a/b');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDelDir('a/b'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDelDir({ path: 'a/b' }));
|
||||
});
|
||||
|
||||
it('deckUpload dispatches correctly', () => {
|
||||
const treeItem = makeDeckTreeItem();
|
||||
Dispatch.deckUpload('a/b', treeItem);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckUpload('a/b', treeItem));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckUpload({ path: 'a/b', treeItem }));
|
||||
});
|
||||
|
||||
it('deckDelete dispatches correctly', () => {
|
||||
Dispatch.deckDelete(42);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDelete(42));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDelete({ deckId: 42 }));
|
||||
});
|
||||
|
||||
it('gamesOfUser dispatches correctly', () => {
|
||||
const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] });
|
||||
Dispatch.gamesOfUser('alice', response);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', response));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gamesOfUser({ userName: 'alice', response }));
|
||||
});
|
||||
|
||||
it('clearRegistrationErrors dispatches correctly', () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const Dispatch = {
|
|||
store.dispatch(Actions.connectionAttempted());
|
||||
},
|
||||
loginSuccessful: (options: Enriched.LoginSuccessContext) => {
|
||||
store.dispatch(Actions.loginSuccessful(options));
|
||||
store.dispatch(Actions.loginSuccessful({ options }));
|
||||
},
|
||||
loginFailed: () => {
|
||||
store.dispatch(Actions.loginFailed());
|
||||
|
|
@ -28,79 +28,73 @@ export const Dispatch = {
|
|||
store.dispatch(Actions.testConnectionFailed());
|
||||
},
|
||||
updateBuddyList: (buddyList: Data.ServerInfo_User[]) => {
|
||||
store.dispatch(Actions.updateBuddyList(buddyList));
|
||||
store.dispatch(Actions.updateBuddyList({ buddyList }));
|
||||
},
|
||||
addToBuddyList: (user: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.addToBuddyList(user));
|
||||
store.dispatch(Actions.addToBuddyList({ user }));
|
||||
},
|
||||
removeFromBuddyList: (userName: string) => {
|
||||
store.dispatch(Actions.removeFromBuddyList(userName));
|
||||
store.dispatch(Actions.removeFromBuddyList({ userName }));
|
||||
},
|
||||
updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => {
|
||||
store.dispatch(Actions.updateIgnoreList(ignoreList));
|
||||
store.dispatch(Actions.updateIgnoreList({ ignoreList }));
|
||||
},
|
||||
addToIgnoreList: (user: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.addToIgnoreList(user));
|
||||
store.dispatch(Actions.addToIgnoreList({ user }));
|
||||
},
|
||||
removeFromIgnoreList: (userName: string) => {
|
||||
store.dispatch(Actions.removeFromIgnoreList(userName));
|
||||
store.dispatch(Actions.removeFromIgnoreList({ userName }));
|
||||
},
|
||||
updateInfo: (name: string, version: string) => {
|
||||
store.dispatch(Actions.updateInfo({
|
||||
name,
|
||||
version
|
||||
}));
|
||||
store.dispatch(Actions.updateInfo({ info: { name, version } }));
|
||||
},
|
||||
updateStatus: (state: App.StatusEnum, description: string) => {
|
||||
store.dispatch(Actions.updateStatus({
|
||||
state,
|
||||
description
|
||||
}));
|
||||
store.dispatch(Actions.updateStatus({ status: { state, description } }));
|
||||
},
|
||||
updateUser: (user: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.updateUser(user));
|
||||
store.dispatch(Actions.updateUser({ user }));
|
||||
},
|
||||
updateUsers: (users: Data.ServerInfo_User[]) => {
|
||||
store.dispatch(Actions.updateUsers(users));
|
||||
store.dispatch(Actions.updateUsers({ users }));
|
||||
},
|
||||
userJoined: (user: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.userJoined(user));
|
||||
store.dispatch(Actions.userJoined({ user }));
|
||||
},
|
||||
userLeft: (name: string) => {
|
||||
store.dispatch(Actions.userLeft(name));
|
||||
store.dispatch(Actions.userLeft({ name }));
|
||||
},
|
||||
viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => {
|
||||
store.dispatch(Actions.viewLogs(logs));
|
||||
store.dispatch(Actions.viewLogs({ logs }));
|
||||
},
|
||||
clearLogs: () => {
|
||||
store.dispatch(Actions.clearLogs());
|
||||
},
|
||||
serverMessage: (message: string) => {
|
||||
store.dispatch(Actions.serverMessage(message));
|
||||
store.dispatch(Actions.serverMessage({ message }));
|
||||
},
|
||||
registrationRequiresEmail: () => {
|
||||
store.dispatch(Actions.registrationRequiresEmail());
|
||||
},
|
||||
registrationSuccess: () => {
|
||||
store.dispatch(Actions.registrationSuccess())
|
||||
store.dispatch(Actions.registrationSuccess());
|
||||
},
|
||||
registrationFailed: (reason: string, endTime?: number) => {
|
||||
store.dispatch(Actions.registrationFailed(reason, endTime));
|
||||
store.dispatch(Actions.registrationFailed({ reason, endTime }));
|
||||
},
|
||||
clearRegistrationErrors: () => {
|
||||
store.dispatch(Actions.clearRegistrationErrors());
|
||||
},
|
||||
registrationEmailError: (error: string) => {
|
||||
store.dispatch(Actions.registrationEmailError(error));
|
||||
store.dispatch(Actions.registrationEmailError({ error }));
|
||||
},
|
||||
registrationPasswordError: (error: string) => {
|
||||
store.dispatch(Actions.registrationPasswordError(error));
|
||||
store.dispatch(Actions.registrationPasswordError({ error }));
|
||||
},
|
||||
registrationUserNameError: (error: string) => {
|
||||
store.dispatch(Actions.registrationUserNameError(error));
|
||||
store.dispatch(Actions.registrationUserNameError({ error }));
|
||||
},
|
||||
accountAwaitingActivation: (options: Enriched.PendingActivationContext) => {
|
||||
store.dispatch(Actions.accountAwaitingActivation(options));
|
||||
store.dispatch(Actions.accountAwaitingActivation({ options }));
|
||||
},
|
||||
accountActivationSuccess: () => {
|
||||
store.dispatch(Actions.accountActivationSuccess());
|
||||
|
|
@ -121,7 +115,7 @@ export const Dispatch = {
|
|||
store.dispatch(Actions.resetPasswordSuccess());
|
||||
},
|
||||
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => {
|
||||
store.dispatch(Actions.adjustMod(userName, shouldBeMod, shouldBeJudge));
|
||||
store.dispatch(Actions.adjustMod({ userName, shouldBeMod, shouldBeJudge }));
|
||||
},
|
||||
reloadConfig: () => {
|
||||
store.dispatch(Actions.reloadConfig());
|
||||
|
|
@ -136,84 +130,84 @@ export const Dispatch = {
|
|||
store.dispatch(Actions.accountPasswordChange());
|
||||
},
|
||||
accountEditChanged: (user: Partial<Data.ServerInfo_User>) => {
|
||||
store.dispatch(Actions.accountEditChanged(user));
|
||||
store.dispatch(Actions.accountEditChanged({ user }));
|
||||
},
|
||||
accountImageChanged: (user: Partial<Data.ServerInfo_User>) => {
|
||||
store.dispatch(Actions.accountImageChanged(user));
|
||||
store.dispatch(Actions.accountImageChanged({ user }));
|
||||
},
|
||||
getUserInfo: (userInfo: Data.ServerInfo_User) => {
|
||||
store.dispatch(Actions.getUserInfo(userInfo));
|
||||
store.dispatch(Actions.getUserInfo({ userInfo }));
|
||||
},
|
||||
notifyUser: (notification: Data.Event_NotifyUser) => {
|
||||
store.dispatch(Actions.notifyUser(notification))
|
||||
store.dispatch(Actions.notifyUser({ notification }));
|
||||
},
|
||||
serverShutdown: (data: Data.Event_ServerShutdown) => {
|
||||
store.dispatch(Actions.serverShutdown(data))
|
||||
store.dispatch(Actions.serverShutdown({ data }));
|
||||
},
|
||||
userMessage: (messageData: Data.Event_UserMessage) => {
|
||||
store.dispatch(Actions.userMessage(messageData))
|
||||
store.dispatch(Actions.userMessage({ messageData }));
|
||||
},
|
||||
addToList: (list: string, userName: string) => {
|
||||
store.dispatch(Actions.addToList(list, userName))
|
||||
store.dispatch(Actions.addToList({ list, userName }));
|
||||
},
|
||||
removeFromList: (list: string, userName: string) => {
|
||||
store.dispatch(Actions.removeFromList(list, userName))
|
||||
store.dispatch(Actions.removeFromList({ list, userName }));
|
||||
},
|
||||
banFromServer: (userName: string) => {
|
||||
store.dispatch(Actions.banFromServer(userName));
|
||||
store.dispatch(Actions.banFromServer({ userName }));
|
||||
},
|
||||
banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => {
|
||||
store.dispatch(Actions.banHistory(userName, banHistory))
|
||||
store.dispatch(Actions.banHistory({ userName, banHistory }));
|
||||
},
|
||||
warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => {
|
||||
store.dispatch(Actions.warnHistory(userName, warnHistory))
|
||||
store.dispatch(Actions.warnHistory({ userName, warnHistory }));
|
||||
},
|
||||
warnListOptions: (warnList: Data.Response_WarnList[]) => {
|
||||
store.dispatch(Actions.warnListOptions(warnList))
|
||||
store.dispatch(Actions.warnListOptions({ warnList }));
|
||||
},
|
||||
warnUser: (userName: string) => {
|
||||
store.dispatch(Actions.warnUser(userName))
|
||||
store.dispatch(Actions.warnUser({ userName }));
|
||||
},
|
||||
grantReplayAccess: (replayId: number, moderatorName: string) => {
|
||||
store.dispatch(Actions.grantReplayAccess(replayId, moderatorName));
|
||||
store.dispatch(Actions.grantReplayAccess({ replayId, moderatorName }));
|
||||
},
|
||||
forceActivateUser: (usernameToActivate: string, moderatorName: string) => {
|
||||
store.dispatch(Actions.forceActivateUser(usernameToActivate, moderatorName));
|
||||
store.dispatch(Actions.forceActivateUser({ usernameToActivate, moderatorName }));
|
||||
},
|
||||
getAdminNotes: (userName: string, notes: string) => {
|
||||
store.dispatch(Actions.getAdminNotes(userName, notes));
|
||||
store.dispatch(Actions.getAdminNotes({ userName, notes }));
|
||||
},
|
||||
updateAdminNotes: (userName: string, notes: string) => {
|
||||
store.dispatch(Actions.updateAdminNotes(userName, notes));
|
||||
store.dispatch(Actions.updateAdminNotes({ userName, notes }));
|
||||
},
|
||||
replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => {
|
||||
store.dispatch(Actions.replayList(matchList));
|
||||
store.dispatch(Actions.replayList({ matchList }));
|
||||
},
|
||||
replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => {
|
||||
store.dispatch(Actions.replayAdded(matchInfo));
|
||||
store.dispatch(Actions.replayAdded({ matchInfo }));
|
||||
},
|
||||
replayModifyMatch: (gameId: number, doNotHide: boolean) => {
|
||||
store.dispatch(Actions.replayModifyMatch(gameId, doNotHide));
|
||||
store.dispatch(Actions.replayModifyMatch({ gameId, doNotHide }));
|
||||
},
|
||||
replayDeleteMatch: (gameId: number) => {
|
||||
store.dispatch(Actions.replayDeleteMatch(gameId));
|
||||
store.dispatch(Actions.replayDeleteMatch({ gameId }));
|
||||
},
|
||||
backendDecks: (deckList: Data.Response_DeckList) => {
|
||||
store.dispatch(Actions.backendDecks(deckList));
|
||||
store.dispatch(Actions.backendDecks({ deckList }));
|
||||
},
|
||||
deckNewDir: (path: string, dirName: string) => {
|
||||
store.dispatch(Actions.deckNewDir(path, dirName));
|
||||
store.dispatch(Actions.deckNewDir({ path, dirName }));
|
||||
},
|
||||
deckDelDir: (path: string) => {
|
||||
store.dispatch(Actions.deckDelDir(path));
|
||||
store.dispatch(Actions.deckDelDir({ path }));
|
||||
},
|
||||
deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => {
|
||||
store.dispatch(Actions.deckUpload(path, treeItem));
|
||||
store.dispatch(Actions.deckUpload({ path, treeItem }));
|
||||
},
|
||||
deckDelete: (deckId: number) => {
|
||||
store.dispatch(Actions.deckDelete(deckId));
|
||||
store.dispatch(Actions.deckDelete({ deckId }));
|
||||
},
|
||||
gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) => {
|
||||
store.dispatch(Actions.gamesOfUser(userName, response));
|
||||
store.dispatch(Actions.gamesOfUser({ userName, response }));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ import { App, Data, Enriched } from '@app/types';
|
|||
|
||||
export interface ServerState {
|
||||
initialized: boolean;
|
||||
buddyList: Data.ServerInfo_User[];
|
||||
ignoreList: Data.ServerInfo_User[];
|
||||
/** Buddies keyed by username for O(1) lookup. Use `getSortedBuddyList` for display. */
|
||||
buddyList: { [userName: string]: Data.ServerInfo_User };
|
||||
/** Ignored users keyed by username for O(1) lookup. Use `getSortedIgnoreList` for display. */
|
||||
ignoreList: { [userName: string]: Data.ServerInfo_User };
|
||||
info: ServerStateInfo;
|
||||
status: ServerStateStatus;
|
||||
logs: ServerStateLogs;
|
||||
user: Data.ServerInfo_User | null;
|
||||
users: Data.ServerInfo_User[];
|
||||
/** Connected users keyed by username for O(1) lookup. Use `getSortedUsers` for display. */
|
||||
users: { [userName: string]: Data.ServerInfo_User };
|
||||
sortUsersBy: ServerStateSortUsersBy;
|
||||
messages: {
|
||||
[userName: string]: Data.Event_UserMessage[];
|
||||
|
|
@ -28,7 +31,8 @@ export interface ServerState {
|
|||
warnListOptions: Data.Response_WarnList[];
|
||||
warnUser: string;
|
||||
adminNotes: { [userName: string]: string };
|
||||
replays: Data.ServerInfo_ReplayMatch[];
|
||||
/** Replays keyed by gameId for O(1) lookup/update. */
|
||||
replays: { [gameId: number]: Data.ServerInfo_ReplayMatch };
|
||||
backendDecks: Data.Response_DeckList | null;
|
||||
gamesOfUser: { [userName: string]: Enriched.Game[] };
|
||||
registrationError: string | null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { App, Data } from '@app/types';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { serverReducer } from './server.reducer';
|
||||
import { Types } from './server.types';
|
||||
import { Actions } from './server.actions';
|
||||
import {
|
||||
makeBanHistoryItem,
|
||||
makePendingActivationContext,
|
||||
|
|
@ -24,22 +24,22 @@ describe('Initialisation', () => {
|
|||
it('returns initialState when called with undefined state', () => {
|
||||
const result = serverReducer(undefined, { type: '@@INIT' });
|
||||
expect(result.initialized).toBe(false);
|
||||
expect(result.buddyList).toEqual([]);
|
||||
expect(result.buddyList).toEqual({});
|
||||
expect(result.status.state).toBe(App.StatusEnum.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('INITIALIZED → resets to initialState with initialized: true', () => {
|
||||
const state = makeServerState({ banUser: 'someone', initialized: false });
|
||||
const result = serverReducer(state, { type: Types.INITIALIZED });
|
||||
const result = serverReducer(state, Actions.initialized());
|
||||
expect(result.initialized).toBe(true);
|
||||
expect(result.banUser).toBe('');
|
||||
expect(result.buddyList).toEqual([]);
|
||||
expect(result.buddyList).toEqual({});
|
||||
});
|
||||
|
||||
it('CLEAR_STORE → resets to initialState but preserves status', () => {
|
||||
const status = { state: App.StatusEnum.LOGGED_IN, description: 'logged in', connectionAttemptMade: true };
|
||||
const state = makeServerState({ status, banUser: 'someone' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_STORE });
|
||||
const result = serverReducer(state, Actions.clearStore());
|
||||
expect(result.banUser).toBe('');
|
||||
expect(result.status).toEqual(status);
|
||||
expect(result.initialized).toBe(false);
|
||||
|
|
@ -48,7 +48,7 @@ describe('Initialisation', () => {
|
|||
it('default → returns state unchanged for unknown action', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: '@@UNKNOWN' });
|
||||
expect(result).toBe(state);
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -57,27 +57,27 @@ describe('Initialisation', () => {
|
|||
describe('Account & Connection', () => {
|
||||
it('CONNECTION_ATTEMPTED → sets connectionAttemptMade to true', () => {
|
||||
const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.DISCONNECTED, description: null } });
|
||||
const result = serverReducer(state, { type: Types.CONNECTION_ATTEMPTED });
|
||||
const result = serverReducer(state, Actions.connectionAttempted());
|
||||
expect(result.status.connectionAttemptMade).toBe(true);
|
||||
});
|
||||
|
||||
it('ACCOUNT_AWAITING_ACTIVATION → returns state unchanged', () => {
|
||||
const options = makePendingActivationContext();
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.accountAwaitingActivation({ options }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_SUCCESS → returns state unchanged', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_SUCCESS });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.accountActivationSuccess());
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_FAILED → returns state unchanged', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_FAILED });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.accountActivationFailed());
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -86,26 +86,26 @@ describe('Account & Connection', () => {
|
|||
describe('Registration', () => {
|
||||
it('REGISTRATION_FAILED → stores normalized error (plain reason)', () => {
|
||||
const state = makeServerState({ registrationError: null });
|
||||
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'Server is disabled', endTime: undefined });
|
||||
const result = serverReducer(state, Actions.registrationFailed({ reason: 'Server is disabled', endTime: undefined }));
|
||||
expect(result.registrationError).toBe('Server is disabled');
|
||||
});
|
||||
|
||||
it('REGISTRATION_FAILED → normalizes banned error when endTime is given', () => {
|
||||
const state = makeServerState({ registrationError: null });
|
||||
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'bad actor', endTime: Date.now() + 100_000 });
|
||||
const result = serverReducer(state, Actions.registrationFailed({ reason: 'bad actor', endTime: Date.now() + 100_000 }));
|
||||
expect(result.registrationError).toContain('banned');
|
||||
expect(result.registrationError).toContain('bad actor');
|
||||
});
|
||||
|
||||
it('CLEAR_REGISTRATION_ERRORS → sets registrationError to null', () => {
|
||||
const state = makeServerState({ registrationError: 'some error' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_REGISTRATION_ERRORS });
|
||||
const result = serverReducer(state, Actions.clearRegistrationErrors());
|
||||
expect(result.registrationError).toBeNull();
|
||||
});
|
||||
|
||||
it('CLEAR_STORE → resets registrationError to null', () => {
|
||||
const state = makeServerState({ registrationError: 'stale error' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_STORE });
|
||||
const result = serverReducer(state, Actions.clearStore());
|
||||
expect(result.registrationError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -115,7 +115,7 @@ describe('Registration', () => {
|
|||
describe('Server Info & Status', () => {
|
||||
it('SERVER_MESSAGE → merges message into state.info', () => {
|
||||
const state = makeServerState({ info: { message: null, name: 'Old', version: '1.0' } });
|
||||
const result = serverReducer(state, { type: Types.SERVER_MESSAGE, message: 'Welcome!' });
|
||||
const result = serverReducer(state, Actions.serverMessage({ message: 'Welcome!' }));
|
||||
expect(result.info.message).toBe('Welcome!');
|
||||
expect(result.info.name).toBe('Old');
|
||||
expect(result.info.version).toBe('1.0');
|
||||
|
|
@ -123,10 +123,7 @@ describe('Server Info & Status', () => {
|
|||
|
||||
it('UPDATE_INFO → merges name and version into state.info (not message)', () => {
|
||||
const state = makeServerState({ info: { message: 'hi', name: null, version: null } });
|
||||
const result = serverReducer(state, {
|
||||
type: Types.UPDATE_INFO,
|
||||
info: { name: 'Servatrice', version: '2.9.0' },
|
||||
});
|
||||
const result = serverReducer(state, Actions.updateInfo({ info: { name: 'Servatrice', version: '2.9.0' } }));
|
||||
expect(result.info.name).toBe('Servatrice');
|
||||
expect(result.info.version).toBe('2.9.0');
|
||||
expect(result.info.message).toBe('hi');
|
||||
|
|
@ -135,7 +132,7 @@ describe('Server Info & Status', () => {
|
|||
it('UPDATE_STATUS → merges state and description into status', () => {
|
||||
const state = makeServerState();
|
||||
const update = { state: App.StatusEnum.LOGGED_IN, description: 'ok' };
|
||||
const result = serverReducer(state, { type: Types.UPDATE_STATUS, status: update });
|
||||
const result = serverReducer(state, Actions.updateStatus({ status: update }));
|
||||
expect(result.status.state).toBe(App.StatusEnum.LOGGED_IN);
|
||||
expect(result.status.description).toBe('ok');
|
||||
expect(result.status.connectionAttemptMade).toBe(false);
|
||||
|
|
@ -145,26 +142,23 @@ describe('Server Info & Status', () => {
|
|||
// ── User ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('User', () => {
|
||||
it('UPDATE_USER → merges action.user into state.user', () => {
|
||||
it('UPDATE_USER → merges action.payload.user into state.user', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice', userLevel: 1 }) });
|
||||
const result = serverReducer(state, {
|
||||
type: Types.UPDATE_USER,
|
||||
user: { userLevel: 8 },
|
||||
});
|
||||
const result = serverReducer(state, Actions.updateUser({ user: { userLevel: 8 } as any }));
|
||||
expect(result.user.name).toBe('Alice');
|
||||
expect(result.user.userLevel).toBe(8);
|
||||
});
|
||||
|
||||
it('ACCOUNT_EDIT_CHANGED → merges action.user into state.user', () => {
|
||||
it('ACCOUNT_EDIT_CHANGED → merges action.payload.user into state.user', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice' }) });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_EDIT_CHANGED, user: { realName: 'Alice Smith' } });
|
||||
const result = serverReducer(state, Actions.accountEditChanged({ user: { realName: 'Alice Smith' } }));
|
||||
expect(result.user.realName).toBe('Alice Smith');
|
||||
expect(result.user.name).toBe('Alice');
|
||||
});
|
||||
|
||||
it('ACCOUNT_IMAGE_CHANGED → merges action.user into state.user', () => {
|
||||
it('ACCOUNT_IMAGE_CHANGED → merges action.payload.user into state.user', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice' }) });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_IMAGE_CHANGED, user: { country: 'US' } });
|
||||
const result = serverReducer(state, Actions.accountImageChanged({ user: { country: 'US' } }));
|
||||
expect(result.user.country).toBe('US');
|
||||
});
|
||||
});
|
||||
|
|
@ -172,74 +166,83 @@ describe('User', () => {
|
|||
// ── Users List ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Users List', () => {
|
||||
it('UPDATE_USERS → replaces users list and sorts by name ASC', () => {
|
||||
it('UPDATE_USERS → replaces users map keyed by name', () => {
|
||||
const state = makeServerState();
|
||||
const users = [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })];
|
||||
const result = serverReducer(state, { type: Types.UPDATE_USERS, users });
|
||||
expect(result.users[0].name).toBe('Alice');
|
||||
expect(result.users[1].name).toBe('Zane');
|
||||
const result = serverReducer(state, Actions.updateUsers({ users }));
|
||||
expect(result.users['Alice']).toBeDefined();
|
||||
expect(result.users['Zane']).toBeDefined();
|
||||
expect(Object.keys(result.users)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('USER_JOINED → appends user and sorts', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Zane' })] });
|
||||
const result = serverReducer(state, { type: Types.USER_JOINED, user: makeUser({ name: 'Alice' }) });
|
||||
expect(result.users[0].name).toBe('Alice');
|
||||
expect(result.users[1].name).toBe('Zane');
|
||||
it('USER_JOINED → inserts user into map', () => {
|
||||
const state = makeServerState({ users: { Zane: makeUser({ name: 'Zane' }) } });
|
||||
const result = serverReducer(state, Actions.userJoined({ user: makeUser({ name: 'Alice' }) }));
|
||||
expect(result.users['Alice']).toBeDefined();
|
||||
expect(result.users['Zane']).toBeDefined();
|
||||
});
|
||||
|
||||
it('USER_LEFT → removes user by name', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
|
||||
const result = serverReducer(state, { type: Types.USER_LEFT, name: 'Alice' });
|
||||
expect(result.users).toHaveLength(1);
|
||||
expect(result.users[0].name).toBe('Bob');
|
||||
it('USER_LEFT → removes user by name from map', () => {
|
||||
const state = makeServerState({
|
||||
users: { Alice: makeUser({ name: 'Alice' }), Bob: makeUser({ name: 'Bob' }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.userLeft({ name: 'Alice' }));
|
||||
expect(result.users['Alice']).toBeUndefined();
|
||||
expect(result.users['Bob']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Buddy & Ignore Lists ──────────────────────────────────────────────────────
|
||||
|
||||
describe('Buddy List', () => {
|
||||
it('UPDATE_BUDDY_LIST → replaces and sorts buddy list', () => {
|
||||
it('UPDATE_BUDDY_LIST → replaces map keyed by name', () => {
|
||||
const state = makeServerState();
|
||||
const buddyList = [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })];
|
||||
const result = serverReducer(state, { type: Types.UPDATE_BUDDY_LIST, buddyList });
|
||||
expect(result.buddyList[0].name).toBe('Alice');
|
||||
const result = serverReducer(state, Actions.updateBuddyList({ buddyList }));
|
||||
expect(result.buddyList['Alice']).toBeDefined();
|
||||
expect(result.buddyList['Zane']).toBeDefined();
|
||||
});
|
||||
|
||||
it('ADD_TO_BUDDY_LIST → appends user and sorts', () => {
|
||||
const state = makeServerState({ buddyList: [makeUser({ name: 'Zane' })] });
|
||||
const result = serverReducer(state, { type: Types.ADD_TO_BUDDY_LIST, user: makeUser({ name: 'Alice' }) });
|
||||
expect(result.buddyList[0].name).toBe('Alice');
|
||||
expect(result.buddyList).toHaveLength(2);
|
||||
it('ADD_TO_BUDDY_LIST → inserts user into map', () => {
|
||||
const state = makeServerState({ buddyList: { Zane: makeUser({ name: 'Zane' }) } });
|
||||
const result = serverReducer(state, Actions.addToBuddyList({ user: makeUser({ name: 'Alice' }) }));
|
||||
expect(result.buddyList['Alice']).toBeDefined();
|
||||
expect(Object.keys(result.buddyList)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('REMOVE_FROM_BUDDY_LIST → removes user by name', () => {
|
||||
const state = makeServerState({ buddyList: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
|
||||
const result = serverReducer(state, { type: Types.REMOVE_FROM_BUDDY_LIST, userName: 'Alice' });
|
||||
expect(result.buddyList).toHaveLength(1);
|
||||
expect(result.buddyList[0].name).toBe('Bob');
|
||||
it('REMOVE_FROM_BUDDY_LIST → removes user by name from map', () => {
|
||||
const state = makeServerState({
|
||||
buddyList: { Alice: makeUser({ name: 'Alice' }), Bob: makeUser({ name: 'Bob' }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.removeFromBuddyList({ userName: 'Alice' }));
|
||||
expect(result.buddyList['Alice']).toBeUndefined();
|
||||
expect(result.buddyList['Bob']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ignore List', () => {
|
||||
it('UPDATE_IGNORE_LIST → replaces and sorts ignore list', () => {
|
||||
it('UPDATE_IGNORE_LIST → replaces map keyed by name', () => {
|
||||
const state = makeServerState();
|
||||
const ignoreList = [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })];
|
||||
const result = serverReducer(state, { type: Types.UPDATE_IGNORE_LIST, ignoreList });
|
||||
expect(result.ignoreList[0].name).toBe('Alice');
|
||||
const result = serverReducer(state, Actions.updateIgnoreList({ ignoreList }));
|
||||
expect(result.ignoreList['Alice']).toBeDefined();
|
||||
expect(result.ignoreList['Zane']).toBeDefined();
|
||||
});
|
||||
|
||||
it('ADD_TO_IGNORE_LIST → appends user and sorts', () => {
|
||||
const state = makeServerState({ ignoreList: [makeUser({ name: 'Zane' })] });
|
||||
const result = serverReducer(state, { type: Types.ADD_TO_IGNORE_LIST, user: makeUser({ name: 'Alice' }) });
|
||||
expect(result.ignoreList[0].name).toBe('Alice');
|
||||
expect(result.ignoreList).toHaveLength(2);
|
||||
it('ADD_TO_IGNORE_LIST → inserts user into map', () => {
|
||||
const state = makeServerState({ ignoreList: { Zane: makeUser({ name: 'Zane' }) } });
|
||||
const result = serverReducer(state, Actions.addToIgnoreList({ user: makeUser({ name: 'Alice' }) }));
|
||||
expect(result.ignoreList['Alice']).toBeDefined();
|
||||
expect(Object.keys(result.ignoreList)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('REMOVE_FROM_IGNORE_LIST → removes user by name', () => {
|
||||
const state = makeServerState({ ignoreList: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
|
||||
const result = serverReducer(state, { type: Types.REMOVE_FROM_IGNORE_LIST, userName: 'Alice' });
|
||||
expect(result.ignoreList).toHaveLength(1);
|
||||
expect(result.ignoreList[0].name).toBe('Bob');
|
||||
it('REMOVE_FROM_IGNORE_LIST → removes user by name from map', () => {
|
||||
const state = makeServerState({
|
||||
ignoreList: { Alice: makeUser({ name: 'Alice' }), Bob: makeUser({ name: 'Bob' }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.removeFromIgnoreList({ userName: 'Alice' }));
|
||||
expect(result.ignoreList['Alice']).toBeUndefined();
|
||||
expect(result.ignoreList['Bob']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -249,13 +252,13 @@ describe('Logs', () => {
|
|||
it('VIEW_LOGS → groups LogItem[] into room/game/chat buckets', () => {
|
||||
const log = makeLogItem({ targetType: 'room' });
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs: [log] });
|
||||
const result = serverReducer(state, Actions.viewLogs({ logs: [log] }));
|
||||
expect(result.logs.room).toEqual([log]);
|
||||
});
|
||||
|
||||
it('CLEAR_LOGS → resets logs to empty arrays', () => {
|
||||
const state = makeServerState({ logs: { room: [makeLogItem()], game: [], chat: [] } });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_LOGS });
|
||||
const result = serverReducer(state, Actions.clearLogs());
|
||||
expect(result.logs.room).toEqual([]);
|
||||
expect(result.logs.game).toEqual([]);
|
||||
expect(result.logs.chat).toEqual([]);
|
||||
|
|
@ -267,18 +270,18 @@ describe('Logs', () => {
|
|||
describe('Messaging', () => {
|
||||
it('USER_MESSAGE → uses receiverName as key when current user is sender', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice' }), messages: {} });
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hi' };
|
||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData });
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hi' } as Data.Event_UserMessage;
|
||||
const result = serverReducer(state, Actions.userMessage({ messageData }));
|
||||
expect(result.messages['Bob']).toHaveLength(1);
|
||||
expect(result.messages['Bob'][0]).toBe(messageData);
|
||||
expect(result.messages['Bob'][0]).toEqual(messageData);
|
||||
});
|
||||
|
||||
it('USER_MESSAGE → uses senderName as key when current user is receiver', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Bob' }), messages: {} });
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'yo' };
|
||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData });
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'yo' } as Data.Event_UserMessage;
|
||||
const result = serverReducer(state, Actions.userMessage({ messageData }));
|
||||
expect(result.messages['Alice']).toHaveLength(1);
|
||||
expect(result.messages['Alice'][0]).toBe(messageData);
|
||||
expect(result.messages['Alice'][0]).toEqual(messageData);
|
||||
});
|
||||
|
||||
it('USER_MESSAGE → appends to existing messages for that user', () => {
|
||||
|
|
@ -288,7 +291,7 @@ describe('Messaging', () => {
|
|||
messages: { Alice: [existingMsg] },
|
||||
});
|
||||
const newMsg = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' });
|
||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
|
||||
const result = serverReducer(state, Actions.userMessage({ messageData: newMsg }));
|
||||
expect(result.messages['Alice']).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -299,23 +302,23 @@ describe('User Info & Notifications', () => {
|
|||
it('GET_USER_INFO → adds userInfo keyed by name', () => {
|
||||
const userInfo = makeUser({ name: 'Eve' });
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.GET_USER_INFO, userInfo });
|
||||
expect(result.userInfo['Eve']).toBe(userInfo);
|
||||
const result = serverReducer(state, Actions.getUserInfo({ userInfo }));
|
||||
expect(result.userInfo['Eve']).toEqual(userInfo);
|
||||
});
|
||||
|
||||
it('NOTIFY_USER → appends notification to list', () => {
|
||||
const state = makeServerState({ notifications: [] });
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
||||
const result = serverReducer(state, { type: Types.NOTIFY_USER, notification });
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' } as unknown as Data.Event_NotifyUser;
|
||||
const result = serverReducer(state, Actions.notifyUser({ notification }));
|
||||
expect(result.notifications).toHaveLength(1);
|
||||
expect(result.notifications[0]).toBe(notification);
|
||||
expect(result.notifications[0]).toEqual(notification);
|
||||
});
|
||||
|
||||
it('SERVER_SHUTDOWN → sets serverShutdown to action.data', () => {
|
||||
const data = { reason: 'maintenance', minutes: 10 };
|
||||
it('SERVER_SHUTDOWN → sets serverShutdown to action.payload.data', () => {
|
||||
const data = { reason: 'maintenance', minutes: 10 } as unknown as Data.Event_ServerShutdown;
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.SERVER_SHUTDOWN, data });
|
||||
expect(result.serverShutdown).toBe(data);
|
||||
const result = serverReducer(state, Actions.serverShutdown({ data }));
|
||||
expect(result.serverShutdown).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -324,46 +327,46 @@ describe('User Info & Notifications', () => {
|
|||
describe('Moderation', () => {
|
||||
it('BAN_FROM_SERVER → sets banUser', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.BAN_FROM_SERVER, userName: 'Frank' });
|
||||
const result = serverReducer(state, Actions.banFromServer({ userName: 'Frank' }));
|
||||
expect(result.banUser).toBe('Frank');
|
||||
});
|
||||
|
||||
it('BAN_HISTORY → adds banHistory keyed by userName', () => {
|
||||
const history = [makeBanHistoryItem()];
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.BAN_HISTORY, userName: 'Frank', banHistory: history });
|
||||
expect(result.banHistory['Frank']).toBe(history);
|
||||
const result = serverReducer(state, Actions.banHistory({ userName: 'Frank', banHistory: history }));
|
||||
expect(result.banHistory['Frank']).toEqual(history);
|
||||
});
|
||||
|
||||
it('WARN_HISTORY → adds warnHistory keyed by userName', () => {
|
||||
const history = [makeWarnHistoryItem()];
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.WARN_HISTORY, userName: 'Grace', warnHistory: history });
|
||||
expect(result.warnHistory['Grace']).toBe(history);
|
||||
const result = serverReducer(state, Actions.warnHistory({ userName: 'Grace', warnHistory: history }));
|
||||
expect(result.warnHistory['Grace']).toEqual(history);
|
||||
});
|
||||
|
||||
it('WARN_LIST_OPTIONS → replaces warnListOptions', () => {
|
||||
const list = [makeWarnListItem()];
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.WARN_LIST_OPTIONS, warnList: list });
|
||||
expect(result.warnListOptions).toBe(list);
|
||||
const result = serverReducer(state, Actions.warnListOptions({ warnList: list }));
|
||||
expect(result.warnListOptions).toEqual(list);
|
||||
});
|
||||
|
||||
it('WARN_USER → sets warnUser', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.WARN_USER, userName: 'Hank' });
|
||||
const result = serverReducer(state, Actions.warnUser({ userName: 'Hank' }));
|
||||
expect(result.warnUser).toBe('Hank');
|
||||
});
|
||||
|
||||
it('GET_ADMIN_NOTES → adds adminNotes keyed by userName', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.GET_ADMIN_NOTES, userName: 'Ira', notes: 'note1' });
|
||||
const result = serverReducer(state, Actions.getAdminNotes({ userName: 'Ira', notes: 'note1' }));
|
||||
expect(result.adminNotes['Ira']).toBe('note1');
|
||||
});
|
||||
|
||||
it('UPDATE_ADMIN_NOTES → updates adminNotes keyed by userName', () => {
|
||||
const state = makeServerState({ adminNotes: { Ira: 'old' } });
|
||||
const result = serverReducer(state, { type: Types.UPDATE_ADMIN_NOTES, userName: 'Ira', notes: 'new' });
|
||||
const result = serverReducer(state, Actions.updateAdminNotes({ userName: 'Ira', notes: 'new' }));
|
||||
expect(result.adminNotes['Ira']).toBe('new');
|
||||
});
|
||||
});
|
||||
|
|
@ -374,87 +377,108 @@ describe('ADJUST_MOD', () => {
|
|||
const baseUserLevel = UserLevelFlag.IsUser | UserLevelFlag.IsRegistered | UserLevelFlag.IsModerator | UserLevelFlag.IsJudge;
|
||||
|
||||
it('shouldBeMod=true, shouldBeJudge=true → sets both bits, preserves IsUser|IsRegistered', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
|
||||
const state = makeServerState({ users: { Dan: makeUser({ name: 'Dan', userLevel: baseUserLevel }) } });
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: true, shouldBeJudge: true }));
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) | IsJudge(16) = 23
|
||||
expect(result.users[0].userLevel).toBe(23);
|
||||
expect(result.users['Dan'].userLevel).toBe(23);
|
||||
});
|
||||
|
||||
it('shouldBeMod=true, shouldBeJudge=false → sets IsModerator, clears IsJudge, preserves others', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||
const state = makeServerState({ users: { Dan: makeUser({ name: 'Dan', userLevel: baseUserLevel }) } });
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: true, shouldBeJudge: false }));
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
|
||||
expect(result.users[0].userLevel).toBe(7);
|
||||
expect(result.users['Dan'].userLevel).toBe(7);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=true → clears IsModerator, sets IsJudge, preserves others', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
|
||||
const state = makeServerState({ users: { Dan: makeUser({ name: 'Dan', userLevel: baseUserLevel }) } });
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: false, shouldBeJudge: true }));
|
||||
// IsUser(1) | IsRegistered(2) | IsJudge(16) = 19
|
||||
expect(result.users[0].userLevel).toBe(19);
|
||||
expect(result.users['Dan'].userLevel).toBe(19);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=false → clears both bits, preserves IsUser|IsRegistered', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
||||
const state = makeServerState({ users: { Dan: makeUser({ name: 'Dan', userLevel: baseUserLevel }) } });
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: false, shouldBeJudge: false }));
|
||||
// IsUser(1) | IsRegistered(2) = 3
|
||||
expect(result.users[0].userLevel).toBe(3);
|
||||
expect(result.users['Dan'].userLevel).toBe(3);
|
||||
});
|
||||
|
||||
it('shouldBeMod=true on IsUser|IsRegistered only → produces 7, not 4', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: UserLevelFlag.IsUser | UserLevelFlag.IsRegistered })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||
const state = makeServerState({
|
||||
users: { Dan: makeUser({ name: 'Dan', userLevel: UserLevelFlag.IsUser | UserLevelFlag.IsRegistered }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: true, shouldBeJudge: false }));
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
|
||||
expect(result.users[0].userLevel).toBe(7);
|
||||
expect(result.users['Dan'].userLevel).toBe(7);
|
||||
});
|
||||
|
||||
it('non-matching users are left unchanged', () => {
|
||||
const alice = makeUser({ name: 'Alice', userLevel: 7 });
|
||||
const state = makeServerState({ users: [alice, makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
||||
expect(result.users.find(u => u.name === 'Alice').userLevel).toBe(7);
|
||||
const state = makeServerState({
|
||||
users: { Alice: alice, Dan: makeUser({ name: 'Dan', userLevel: baseUserLevel }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Dan', shouldBeMod: false, shouldBeJudge: false }));
|
||||
expect(result.users['Alice']).toEqual(alice);
|
||||
});
|
||||
|
||||
it('unknown userName → state unchanged', () => {
|
||||
const state = makeServerState({ users: { Dan: makeUser({ name: 'Dan' }) } });
|
||||
const result = serverReducer(state, Actions.adjustMod({ userName: 'Ghost', shouldBeMod: true, shouldBeJudge: false }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Replays ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Replays', () => {
|
||||
it('REPLAY_LIST → replaces replays list', () => {
|
||||
it('REPLAY_LIST → replaces replays map keyed by gameId', () => {
|
||||
const matchList = [makeReplayMatch({ gameId: 10 })];
|
||||
const state = makeServerState({ replays: [makeReplayMatch({ gameId: 99 })] });
|
||||
const result = serverReducer(state, { type: Types.REPLAY_LIST, matchList });
|
||||
expect(result.replays).toHaveLength(1);
|
||||
expect(result.replays[0].gameId).toBe(10);
|
||||
const state = makeServerState({ replays: { 99: makeReplayMatch({ gameId: 99 }) } });
|
||||
const result = serverReducer(state, Actions.replayList({ matchList }));
|
||||
expect(Object.keys(result.replays)).toHaveLength(1);
|
||||
expect(result.replays[10]).toBeDefined();
|
||||
expect(result.replays[99]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('REPLAY_ADDED → appends matchInfo to replays', () => {
|
||||
it('REPLAY_ADDED → inserts matchInfo into replays map', () => {
|
||||
const existing = makeReplayMatch({ gameId: 1 });
|
||||
const added = makeReplayMatch({ gameId: 2 });
|
||||
const state = makeServerState({ replays: [existing] });
|
||||
const result = serverReducer(state, { type: Types.REPLAY_ADDED, matchInfo: added });
|
||||
expect(result.replays).toHaveLength(2);
|
||||
expect(result.replays[1]).toBe(added);
|
||||
const state = makeServerState({ replays: { 1: existing } });
|
||||
const result = serverReducer(state, Actions.replayAdded({ matchInfo: added }));
|
||||
expect(Object.keys(result.replays)).toHaveLength(2);
|
||||
expect(result.replays[2]).toEqual(added);
|
||||
});
|
||||
|
||||
it('REPLAY_MODIFY_MATCH → updates doNotHide for matching gameId', () => {
|
||||
const state = makeServerState({ replays: [makeReplayMatch({ gameId: 5, doNotHide: false })] });
|
||||
const result = serverReducer(state, { type: Types.REPLAY_MODIFY_MATCH, gameId: 5, doNotHide: true });
|
||||
expect(result.replays[0].doNotHide).toBe(true);
|
||||
const state = makeServerState({ replays: { 5: makeReplayMatch({ gameId: 5, doNotHide: false }) } });
|
||||
const result = serverReducer(state, Actions.replayModifyMatch({ gameId: 5, doNotHide: true }));
|
||||
expect(result.replays[5].doNotHide).toBe(true);
|
||||
});
|
||||
|
||||
it('REPLAY_MODIFY_MATCH → leaves non-matching replays unchanged', () => {
|
||||
const r1 = makeReplayMatch({ gameId: 1, doNotHide: false });
|
||||
const r2 = makeReplayMatch({ gameId: 2, doNotHide: false });
|
||||
const state = makeServerState({ replays: [r1, r2] });
|
||||
const result = serverReducer(state, { type: Types.REPLAY_MODIFY_MATCH, gameId: 1, doNotHide: true });
|
||||
expect(result.replays[1].doNotHide).toBe(false);
|
||||
const state = makeServerState({ replays: { 1: r1, 2: r2 } });
|
||||
const result = serverReducer(state, Actions.replayModifyMatch({ gameId: 1, doNotHide: true }));
|
||||
expect(result.replays[2]).toEqual(r2);
|
||||
expect(result.replays[2].doNotHide).toBe(false);
|
||||
});
|
||||
|
||||
it('REPLAY_MODIFY_MATCH → unknown gameId → state unchanged', () => {
|
||||
const state = makeServerState({ replays: { 5: makeReplayMatch({ gameId: 5 }) } });
|
||||
const result = serverReducer(state, Actions.replayModifyMatch({ gameId: 999, doNotHide: true }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('REPLAY_DELETE_MATCH → removes replay by gameId', () => {
|
||||
const state = makeServerState({ replays: [makeReplayMatch({ gameId: 5 }), makeReplayMatch({ gameId: 6 })] });
|
||||
const result = serverReducer(state, { type: Types.REPLAY_DELETE_MATCH, gameId: 5 });
|
||||
expect(result.replays).toHaveLength(1);
|
||||
expect(result.replays[0].gameId).toBe(6);
|
||||
const state = makeServerState({
|
||||
replays: { 5: makeReplayMatch({ gameId: 5 }), 6: makeReplayMatch({ gameId: 6 }) },
|
||||
});
|
||||
const result = serverReducer(state, Actions.replayDeleteMatch({ gameId: 5 }));
|
||||
expect(Object.keys(result.replays)).toHaveLength(1);
|
||||
expect(result.replays[5]).toBeUndefined();
|
||||
expect(result.replays[6]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -464,22 +488,22 @@ describe('Deck Storage', () => {
|
|||
it('BACKEND_DECKS → sets backendDecks', () => {
|
||||
const deckList = makeDeckList();
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.BACKEND_DECKS, deckList });
|
||||
expect(result.backendDecks).toBe(deckList);
|
||||
const result = serverReducer(state, Actions.backendDecks({ deckList }));
|
||||
expect(result.backendDecks).toEqual(deckList);
|
||||
});
|
||||
|
||||
it('DECK_UPLOAD with null backendDecks → returns state unchanged', () => {
|
||||
const state = makeServerState({ backendDecks: null });
|
||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: '', treeItem: makeDeckTreeItem() });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.deckUpload({ path: '', treeItem: makeDeckTreeItem() }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('DECK_UPLOAD with flat path → appends item to root', () => {
|
||||
const state = makeServerState({ backendDecks: makeDeckList() });
|
||||
const item = makeDeckTreeItem({ name: 'deck.cod' });
|
||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: '', treeItem: item });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
expect(result.backendDecks.root.items[0]).toBe(item);
|
||||
const result = serverReducer(state, Actions.deckUpload({ path: '', treeItem: item }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(1);
|
||||
expect(result.backendDecks!.root!.items[0]).toEqual(item);
|
||||
});
|
||||
|
||||
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
|
||||
|
|
@ -490,25 +514,25 @@ describe('Deck Storage', () => {
|
|||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const item = makeDeckTreeItem({ name: 'new.cod' });
|
||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item });
|
||||
const folder = result.backendDecks.root.items.find(i => i.name === 'myDecks');
|
||||
expect(folder.folder.items).toHaveLength(1);
|
||||
expect(folder.folder.items[0]).toBe(item);
|
||||
const result = serverReducer(state, Actions.deckUpload({ path: 'myDecks', treeItem: item }));
|
||||
const folder = result.backendDecks!.root!.items.find(i => i.name === 'myDecks');
|
||||
expect(folder!.folder!.items).toHaveLength(1);
|
||||
expect(folder!.folder!.items[0]).toEqual(item);
|
||||
});
|
||||
|
||||
it('DECK_UPLOAD with non-existent intermediate folder → creates folder and inserts', () => {
|
||||
const state = makeServerState({ backendDecks: makeDeckList() });
|
||||
const item = makeDeckTreeItem({ name: 'deck.cod' });
|
||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'newFolder', treeItem: item });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
expect(result.backendDecks.root.items[0].name).toBe('newFolder');
|
||||
expect(result.backendDecks.root.items[0].folder.items[0]).toBe(item);
|
||||
const result = serverReducer(state, Actions.deckUpload({ path: 'newFolder', treeItem: item }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(1);
|
||||
expect(result.backendDecks!.root!.items[0].name).toBe('newFolder');
|
||||
expect(result.backendDecks!.root!.items[0].folder!.items[0]).toEqual(item);
|
||||
});
|
||||
|
||||
it('DECK_DELETE with null backendDecks → returns state unchanged', () => {
|
||||
const state = makeServerState({ backendDecks: null });
|
||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 1 });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.deckDelete({ deckId: 1 }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('DECK_DELETE → removes item by id from tree', () => {
|
||||
|
|
@ -516,8 +540,8 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }),
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 });
|
||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||
const result = serverReducer(state, Actions.deckDelete({ deckId: 7 }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('DECK_DELETE → recursively removes item nested inside a subfolder', () => {
|
||||
|
|
@ -528,22 +552,22 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 9 });
|
||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
||||
const result = serverReducer(state, Actions.deckDelete({ deckId: 9 }));
|
||||
expect(result.backendDecks!.root!.items[0].folder!.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('DECK_NEW_DIR with null backendDecks → returns state unchanged', () => {
|
||||
const state = makeServerState({ backendDecks: null });
|
||||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: '', dirName: 'newDir' });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.deckNewDir({ path: '', dirName: 'newDir' }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('DECK_NEW_DIR at root → appends folder to root items', () => {
|
||||
const state = makeServerState({ backendDecks: makeDeckList() });
|
||||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: '', dirName: 'myDir' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
expect(result.backendDecks.root.items[0].name).toBe('myDir');
|
||||
expect(result.backendDecks.root.items[0].folder.items).toEqual([]);
|
||||
const result = serverReducer(state, Actions.deckNewDir({ path: '', dirName: 'myDir' }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(1);
|
||||
expect(result.backendDecks!.root!.items[0].name).toBe('myDir');
|
||||
expect(result.backendDecks!.root!.items[0].folder!.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
|
||||
|
|
@ -553,16 +577,16 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: 'parent', dirName: 'child' });
|
||||
const parent = result.backendDecks.root.items.find(i => i.name === 'parent');
|
||||
expect(parent.folder.items).toHaveLength(1);
|
||||
expect(parent.folder.items[0].name).toBe('child');
|
||||
const result = serverReducer(state, Actions.deckNewDir({ path: 'parent', dirName: 'child' }));
|
||||
const parent = result.backendDecks!.root!.items.find(i => i.name === 'parent');
|
||||
expect(parent!.folder!.items).toHaveLength(1);
|
||||
expect(parent!.folder!.items[0].name).toBe('child');
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR with null backendDecks → returns state unchanged', () => {
|
||||
const state = makeServerState({ backendDecks: null });
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
|
||||
expect(result).toBe(state);
|
||||
const result = serverReducer(state, Actions.deckDelDir({ path: 'myDir' }));
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → removes folder from root by name', () => {
|
||||
|
|
@ -572,8 +596,8 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||
const result = serverReducer(state, Actions.deckDelDir({ path: 'myDir' }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => {
|
||||
|
|
@ -583,8 +607,8 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
const result = serverReducer(state, Actions.deckDelDir({ path: '' }));
|
||||
expect(result.backendDecks!.root!.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => {
|
||||
|
|
@ -597,8 +621,8 @@ describe('Deck Storage', () => {
|
|||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) })
|
||||
});
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' });
|
||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
||||
const result = serverReducer(state, Actions.deckDelDir({ path: 'parent/child' }));
|
||||
expect(result.backendDecks!.root!.items[0].folder!.items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -611,7 +635,7 @@ describe('GAMES_OF_USER', () => {
|
|||
roomList: [],
|
||||
});
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
|
||||
const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response }));
|
||||
expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 5 })]);
|
||||
});
|
||||
|
||||
|
|
@ -622,7 +646,7 @@ describe('GAMES_OF_USER', () => {
|
|||
roomList: [],
|
||||
});
|
||||
const state = makeServerState({ gamesOfUser: { alice: old } });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
|
||||
const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response }));
|
||||
expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 2 })]);
|
||||
});
|
||||
|
||||
|
|
@ -630,7 +654,7 @@ describe('GAMES_OF_USER', () => {
|
|||
const bobGames = [makeGame({ gameId: 3 })];
|
||||
const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [], roomList: [] });
|
||||
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
|
||||
expect(result.gamesOfUser['bob']).toBe(bobGames);
|
||||
const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response }));
|
||||
expect(result.gamesOfUser['bob']).toEqual(bobGames);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { App, Data } from '@app/types';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
|
||||
import { normalizeBannedUserError, normalizeGameObject, normalizeGametypeMap, normalizeLogs, SortUtil } from '../common';
|
||||
import { normalizeBannedUserError, normalizeGameObject, normalizeGametypeMap, normalizeLogs } from '../common';
|
||||
|
||||
import { ServerAction } from './server.actions';
|
||||
import { ServerState } from './server.interfaces'
|
||||
import { Types } from './server.types';
|
||||
import { ServerState, ServerStateStatus } from './server.interfaces';
|
||||
|
||||
function splitPath(path: string): string[] {
|
||||
return path ? path.split('/') : [];
|
||||
|
|
@ -67,8 +66,8 @@ function removeByPath(folder: Data.ServerInfo_DeckStorage_Folder, pathSegments:
|
|||
|
||||
const initialState: ServerState = {
|
||||
initialized: false,
|
||||
buddyList: [],
|
||||
ignoreList: [],
|
||||
buddyList: {},
|
||||
ignoreList: {},
|
||||
|
||||
status: {
|
||||
connectionAttemptMade: false,
|
||||
|
|
@ -86,7 +85,7 @@ const initialState: ServerState = {
|
|||
chat: []
|
||||
},
|
||||
user: null,
|
||||
users: [],
|
||||
users: {},
|
||||
sortUsersBy: {
|
||||
field: App.UserSortField.NAME,
|
||||
order: App.SortDirection.ASC
|
||||
|
|
@ -101,452 +100,303 @@ const initialState: ServerState = {
|
|||
warnListOptions: [],
|
||||
warnUser: '',
|
||||
adminNotes: {},
|
||||
replays: [],
|
||||
replays: {},
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
registrationError: null,
|
||||
};
|
||||
|
||||
export const serverReducer = (state = initialState, action: ServerAction) => {
|
||||
switch (action.type) {
|
||||
case Types.INITIALIZED: {
|
||||
return {
|
||||
...initialState,
|
||||
initialized: true
|
||||
export const serverSlice = createSlice({
|
||||
name: 'server',
|
||||
initialState,
|
||||
reducers: {
|
||||
initialized: () => ({
|
||||
...initialState,
|
||||
initialized: true,
|
||||
}),
|
||||
|
||||
connectionAttempted: (state) => {
|
||||
state.status.connectionAttemptMade = true;
|
||||
},
|
||||
|
||||
clearStore: (state) => ({
|
||||
...initialState,
|
||||
status: { ...state.status },
|
||||
}),
|
||||
|
||||
serverMessage: (state, action: PayloadAction<{ message: string }>) => {
|
||||
state.info.message = action.payload.message;
|
||||
},
|
||||
|
||||
updateBuddyList: (state, action: PayloadAction<{ buddyList: Data.ServerInfo_User[] }>) => {
|
||||
const buddyList: { [userName: string]: Data.ServerInfo_User } = {};
|
||||
for (const user of action.payload.buddyList) {
|
||||
buddyList[user.name] = user;
|
||||
}
|
||||
}
|
||||
case Types.CONNECTION_ATTEMPTED: {
|
||||
return {
|
||||
...state,
|
||||
status: { ...state.status, connectionAttemptMade: true }
|
||||
};
|
||||
}
|
||||
case Types.ACCOUNT_AWAITING_ACTIVATION: {
|
||||
return state;
|
||||
}
|
||||
case Types.ACCOUNT_ACTIVATION_FAILED:
|
||||
case Types.ACCOUNT_ACTIVATION_SUCCESS: {
|
||||
return state;
|
||||
}
|
||||
case Types.CLEAR_STORE: {
|
||||
return {
|
||||
...initialState,
|
||||
status: {
|
||||
...state.status
|
||||
}
|
||||
state.buddyList = buddyList;
|
||||
},
|
||||
|
||||
addToBuddyList: (state, action: PayloadAction<{ user: Data.ServerInfo_User }>) => {
|
||||
const { user } = action.payload;
|
||||
state.buddyList[user.name] = user;
|
||||
},
|
||||
|
||||
removeFromBuddyList: (state, action: PayloadAction<{ userName: string }>) => {
|
||||
delete state.buddyList[action.payload.userName];
|
||||
},
|
||||
|
||||
updateIgnoreList: (state, action: PayloadAction<{ ignoreList: Data.ServerInfo_User[] }>) => {
|
||||
const ignoreList: { [userName: string]: Data.ServerInfo_User } = {};
|
||||
for (const user of action.payload.ignoreList) {
|
||||
ignoreList[user.name] = user;
|
||||
}
|
||||
}
|
||||
case Types.SERVER_MESSAGE: {
|
||||
const { message } = action;
|
||||
const { info } = state;
|
||||
state.ignoreList = ignoreList;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
info: { ...info, message }
|
||||
}
|
||||
}
|
||||
case Types.UPDATE_BUDDY_LIST: {
|
||||
const { buddyList } = action;
|
||||
const { sortUsersBy } = state;
|
||||
addToIgnoreList: (state, action: PayloadAction<{ user: Data.ServerInfo_User }>) => {
|
||||
const { user } = action.payload;
|
||||
state.ignoreList[user.name] = user;
|
||||
},
|
||||
|
||||
SortUtil.sortUsersByField(buddyList, sortUsersBy);
|
||||
removeFromIgnoreList: (state, action: PayloadAction<{ userName: string }>) => {
|
||||
delete state.ignoreList[action.payload.userName];
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
buddyList: [
|
||||
...buddyList
|
||||
]
|
||||
};
|
||||
}
|
||||
case Types.ADD_TO_BUDDY_LIST: {
|
||||
const { user } = action;
|
||||
const { sortUsersBy } = state;
|
||||
updateInfo: (state, action: PayloadAction<{ info: { name: string; version: string } }>) => {
|
||||
const { name, version } = action.payload.info;
|
||||
state.info.name = name;
|
||||
state.info.version = version;
|
||||
},
|
||||
|
||||
const buddyList = [...state.buddyList];
|
||||
|
||||
buddyList.push(user);
|
||||
SortUtil.sortUsersByField(buddyList, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
buddyList
|
||||
};
|
||||
}
|
||||
case Types.REMOVE_FROM_BUDDY_LIST: {
|
||||
const { userName } = action;
|
||||
const buddyList = state.buddyList.filter(user => user.name !== userName);
|
||||
|
||||
return {
|
||||
...state,
|
||||
buddyList
|
||||
};
|
||||
}
|
||||
case Types.UPDATE_IGNORE_LIST: {
|
||||
const { ignoreList } = action;
|
||||
const { sortUsersBy } = state;
|
||||
|
||||
SortUtil.sortUsersByField(ignoreList, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
ignoreList: [
|
||||
...ignoreList
|
||||
]
|
||||
};
|
||||
}
|
||||
case Types.ADD_TO_IGNORE_LIST: {
|
||||
const { user } = action;
|
||||
const { sortUsersBy } = state;
|
||||
|
||||
const ignoreList = [...state.ignoreList];
|
||||
|
||||
ignoreList.push(user);
|
||||
SortUtil.sortUsersByField(ignoreList, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
ignoreList
|
||||
};
|
||||
}
|
||||
case Types.REMOVE_FROM_IGNORE_LIST: {
|
||||
const { userName } = action;
|
||||
const ignoreList = state.ignoreList.filter(user => user.name !== userName);
|
||||
|
||||
return {
|
||||
...state,
|
||||
ignoreList
|
||||
};
|
||||
}
|
||||
case Types.UPDATE_INFO: {
|
||||
const { name, version } = action.info;
|
||||
const { info } = state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
info: { ...info, name, version }
|
||||
}
|
||||
}
|
||||
case Types.UPDATE_STATUS: {
|
||||
const { status } = action;
|
||||
const newState = {
|
||||
...state,
|
||||
status: { ...state.status, ...status }
|
||||
};
|
||||
updateStatus: (state, action: PayloadAction<{ status: Pick<ServerStateStatus, 'state' | 'description'> }>) => {
|
||||
const { status } = action.payload;
|
||||
state.status = { ...state.status, ...status };
|
||||
|
||||
if (status.state === App.StatusEnum.DISCONNECTED) {
|
||||
return {
|
||||
...newState,
|
||||
status: { ...newState.status, connectionAttemptMade: false }
|
||||
};
|
||||
state.status.connectionAttemptMade = false;
|
||||
}
|
||||
},
|
||||
|
||||
return newState;
|
||||
}
|
||||
case Types.UPDATE_USER:
|
||||
case Types.ACCOUNT_EDIT_CHANGED:
|
||||
case Types.ACCOUNT_IMAGE_CHANGED: {
|
||||
const { user } = action;
|
||||
updateUser: (state, action: PayloadAction<{ user: Data.ServerInfo_User | Partial<Data.ServerInfo_User> }>) => {
|
||||
state.user = { ...state.user, ...action.payload.user } as Data.ServerInfo_User;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
...user
|
||||
}
|
||||
updateUsers: (state, action: PayloadAction<{ users: Data.ServerInfo_User[] }>) => {
|
||||
const users: { [userName: string]: Data.ServerInfo_User } = {};
|
||||
for (const user of action.payload.users) {
|
||||
users[user.name] = user;
|
||||
}
|
||||
}
|
||||
case Types.UPDATE_USERS: {
|
||||
const users = [...action.users];
|
||||
const { sortUsersBy } = state;
|
||||
state.users = users;
|
||||
},
|
||||
|
||||
userJoined: (state, action: PayloadAction<{ user: Data.ServerInfo_User }>) => {
|
||||
const { user } = action.payload;
|
||||
state.users[user.name] = user;
|
||||
},
|
||||
|
||||
SortUtil.sortUsersByField(users, sortUsersBy);
|
||||
userLeft: (state, action: PayloadAction<{ name: string }>) => {
|
||||
delete state.users[action.payload.name];
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
users
|
||||
};
|
||||
}
|
||||
case Types.USER_JOINED: {
|
||||
const { sortUsersBy } = state;
|
||||
viewLogs: (state, action: PayloadAction<{ logs: Data.ServerInfo_ChatMessage[] }>) => {
|
||||
state.logs = { ...normalizeLogs(action.payload.logs) };
|
||||
},
|
||||
|
||||
const users = [
|
||||
...state.users,
|
||||
{ ...action.user }
|
||||
];
|
||||
clearLogs: (state) => {
|
||||
state.logs = { ...initialState.logs };
|
||||
},
|
||||
|
||||
SortUtil.sortUsersByField(users, sortUsersBy);
|
||||
|
||||
return {
|
||||
...state,
|
||||
users
|
||||
};
|
||||
}
|
||||
case Types.USER_LEFT: {
|
||||
const { name } = action;
|
||||
const users = state.users.filter(user => user.name !== name);
|
||||
|
||||
return {
|
||||
...state,
|
||||
users
|
||||
};
|
||||
}
|
||||
case Types.VIEW_LOGS: {
|
||||
const { logs } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
logs: {
|
||||
...normalizeLogs(logs)
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.CLEAR_LOGS: {
|
||||
return {
|
||||
...state,
|
||||
logs: {
|
||||
...initialState.logs
|
||||
}
|
||||
userMessage: (state, action: PayloadAction<{ messageData: Data.Event_UserMessage }>) => {
|
||||
const { senderName, receiverName } = action.payload.messageData;
|
||||
const userName = state.user!.name === senderName ? receiverName : senderName;
|
||||
if (!state.messages[userName]) {
|
||||
state.messages[userName] = [];
|
||||
}
|
||||
}
|
||||
case Types.USER_MESSAGE: {
|
||||
const { senderName, receiverName } = action.messageData;
|
||||
const userName = state.user.name === senderName ? receiverName : senderName;
|
||||
state.messages[userName].push(action.payload.messageData);
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...state.messages,
|
||||
[userName]: [
|
||||
...(state.messages[userName] ?? []),
|
||||
action.messageData,
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.GET_USER_INFO: {
|
||||
const { userInfo } = action;
|
||||
getUserInfo: (state, action: PayloadAction<{ userInfo: Data.ServerInfo_User }>) => {
|
||||
const { userInfo } = action.payload;
|
||||
state.userInfo[userInfo.name] = userInfo;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
userInfo: {
|
||||
...state.userInfo,
|
||||
[userInfo.name]: userInfo,
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.NOTIFY_USER: {
|
||||
const { notification } = action;
|
||||
notifyUser: (state, action: PayloadAction<{ notification: Data.Event_NotifyUser }>) => {
|
||||
state.notifications.push(action.payload.notification);
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
notifications: [
|
||||
...state.notifications,
|
||||
notification
|
||||
]
|
||||
};
|
||||
}
|
||||
case Types.SERVER_SHUTDOWN: {
|
||||
const { data } = action;
|
||||
serverShutdown: (state, action: PayloadAction<{ data: Data.Event_ServerShutdown }>) => {
|
||||
state.serverShutdown = action.payload.data;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
serverShutdown: data,
|
||||
};
|
||||
}
|
||||
case Types.BAN_FROM_SERVER: {
|
||||
const { userName } = action;
|
||||
banFromServer: (state, action: PayloadAction<{ userName: string }>) => {
|
||||
state.banUser = action.payload.userName;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
banUser: userName,
|
||||
};
|
||||
}
|
||||
case Types.BAN_HISTORY: {
|
||||
const { userName, banHistory } = action;
|
||||
banHistory: (state, action: PayloadAction<{ userName: string; banHistory: Data.ServerInfo_Ban[] }>) => {
|
||||
state.banHistory[action.payload.userName] = action.payload.banHistory;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
banHistory: {
|
||||
...state.banHistory,
|
||||
[userName]: banHistory,
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.WARN_HISTORY: {
|
||||
const { userName, warnHistory } = action;
|
||||
warnHistory: (state, action: PayloadAction<{ userName: string; warnHistory: Data.ServerInfo_Warning[] }>) => {
|
||||
state.warnHistory[action.payload.userName] = action.payload.warnHistory;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
warnHistory: {
|
||||
...state.warnHistory,
|
||||
[userName]: warnHistory,
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.WARN_LIST_OPTIONS: {
|
||||
const { warnList } = action;
|
||||
warnListOptions: (state, action: PayloadAction<{ warnList: Data.Response_WarnList[] }>) => {
|
||||
state.warnListOptions = action.payload.warnList;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
warnListOptions: warnList,
|
||||
};
|
||||
}
|
||||
case Types.WARN_USER: {
|
||||
const { userName } = action;
|
||||
return {
|
||||
...state,
|
||||
warnUser: userName,
|
||||
};
|
||||
}
|
||||
case Types.GET_ADMIN_NOTES:
|
||||
case Types.UPDATE_ADMIN_NOTES: {
|
||||
const { userName, notes } = action;
|
||||
return {
|
||||
...state,
|
||||
adminNotes: {
|
||||
...state.adminNotes,
|
||||
[userName]: notes,
|
||||
}
|
||||
};
|
||||
}
|
||||
case Types.ADJUST_MOD: {
|
||||
const { userName, shouldBeMod, shouldBeJudge } = action;
|
||||
warnUser: (state, action: PayloadAction<{ userName: string }>) => {
|
||||
state.warnUser = action.payload.userName;
|
||||
},
|
||||
|
||||
return {
|
||||
...state,
|
||||
users: state.users.map((user) => {
|
||||
if (user.name !== userName) {
|
||||
return user;
|
||||
}
|
||||
let newLevel = user.userLevel;
|
||||
newLevel = shouldBeMod
|
||||
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsModerator)
|
||||
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsModerator);
|
||||
newLevel = shouldBeJudge
|
||||
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsJudge)
|
||||
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsJudge);
|
||||
return {
|
||||
...user,
|
||||
userLevel: newLevel,
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
case Types.REPLAY_LIST: {
|
||||
return { ...state, replays: [...action.matchList] };
|
||||
}
|
||||
case Types.REPLAY_ADDED: {
|
||||
return { ...state, replays: [...state.replays, action.matchInfo] };
|
||||
}
|
||||
case Types.REPLAY_MODIFY_MATCH: {
|
||||
return {
|
||||
...state,
|
||||
replays: state.replays.map(r =>
|
||||
r.gameId === action.gameId ? { ...r, doNotHide: action.doNotHide } : r
|
||||
),
|
||||
};
|
||||
}
|
||||
case Types.REPLAY_DELETE_MATCH: {
|
||||
return { ...state, replays: state.replays.filter(r => r.gameId !== action.gameId) };
|
||||
}
|
||||
case Types.BACKEND_DECKS: {
|
||||
return { ...state, backendDecks: action.deckList };
|
||||
}
|
||||
case Types.DECK_UPLOAD: {
|
||||
getAdminNotes: (state, action: PayloadAction<{ userName: string; notes: string }>) => {
|
||||
state.adminNotes[action.payload.userName] = action.payload.notes;
|
||||
},
|
||||
|
||||
updateAdminNotes: (state, action: PayloadAction<{ userName: string; notes: string }>) => {
|
||||
state.adminNotes[action.payload.userName] = action.payload.notes;
|
||||
},
|
||||
|
||||
adjustMod: (state, action: PayloadAction<{ userName: string; shouldBeMod: boolean; shouldBeJudge: boolean }>) => {
|
||||
const { userName, shouldBeMod, shouldBeJudge } = action.payload;
|
||||
const user = state.users[userName];
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
let newLevel = user.userLevel;
|
||||
newLevel = shouldBeMod
|
||||
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsModerator)
|
||||
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsModerator);
|
||||
newLevel = shouldBeJudge
|
||||
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsJudge)
|
||||
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsJudge);
|
||||
state.users[userName] = { ...user, userLevel: newLevel };
|
||||
},
|
||||
|
||||
replayList: (state, action: PayloadAction<{ matchList: Data.ServerInfo_ReplayMatch[] }>) => {
|
||||
const replays: { [gameId: number]: Data.ServerInfo_ReplayMatch } = {};
|
||||
for (const match of action.payload.matchList) {
|
||||
replays[match.gameId] = match;
|
||||
}
|
||||
state.replays = replays;
|
||||
},
|
||||
|
||||
replayAdded: (state, action: PayloadAction<{ matchInfo: Data.ServerInfo_ReplayMatch }>) => {
|
||||
const { matchInfo } = action.payload;
|
||||
state.replays[matchInfo.gameId] = matchInfo;
|
||||
},
|
||||
|
||||
replayModifyMatch: (state, action: PayloadAction<{ gameId: number; doNotHide: boolean }>) => {
|
||||
const { gameId, doNotHide } = action.payload;
|
||||
const existing = state.replays[gameId];
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
state.replays[gameId] = { ...existing, doNotHide };
|
||||
},
|
||||
|
||||
replayDeleteMatch: (state, action: PayloadAction<{ gameId: number }>) => {
|
||||
delete state.replays[action.payload.gameId];
|
||||
},
|
||||
|
||||
backendDecks: (state, action: PayloadAction<{ deckList: Data.Response_DeckList }>) => {
|
||||
state.backendDecks = action.payload.deckList;
|
||||
},
|
||||
|
||||
deckUpload: (state, action: PayloadAction<{ path: string; treeItem: Data.ServerInfo_DeckStorage_TreeItem }>) => {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: create(Data.Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_DELETE: {
|
||||
state.backendDecks = create(Data.Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.payload.path), action.payload.treeItem),
|
||||
});
|
||||
},
|
||||
|
||||
deckDelete: (state, action: PayloadAction<{ deckId: number }>) => {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: create(Data.Response_DeckListSchema, {
|
||||
root: removeById(state.backendDecks.root, action.deckId),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_NEW_DIR: {
|
||||
state.backendDecks = create(Data.Response_DeckListSchema, {
|
||||
root: removeById(state.backendDecks.root, action.payload.deckId),
|
||||
});
|
||||
},
|
||||
|
||||
deckNewDir: (state, action: PayloadAction<{ path: string; dirName: string }>) => {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
const newFolder: Data.ServerInfo_DeckStorage_TreeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: action.dirName, folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
id: 0, name: action.payload.dirName, folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
backendDecks: create(Data.Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_DEL_DIR: {
|
||||
state.backendDecks = create(Data.Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.payload.path), newFolder),
|
||||
});
|
||||
},
|
||||
|
||||
deckDelDir: (state, action: PayloadAction<{ path: string }>) => {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: create(Data.Response_DeckListSchema, {
|
||||
root: removeByPath(state.backendDecks.root, splitPath(action.path)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.GAMES_OF_USER: {
|
||||
const { userName, response } = action;
|
||||
state.backendDecks = create(Data.Response_DeckListSchema, {
|
||||
root: removeByPath(state.backendDecks.root, splitPath(action.payload.path)),
|
||||
});
|
||||
},
|
||||
|
||||
gamesOfUser: (state, action: PayloadAction<{ userName: string; response: Data.Response_GetGamesOfUser }>) => {
|
||||
const { userName, response } = action.payload;
|
||||
const gametypeMap = normalizeGametypeMap(
|
||||
(response.roomList ?? []).flatMap(room => room.gametypeList ?? [])
|
||||
);
|
||||
const normalizedGames = (response.gameList ?? []).map(g => normalizeGameObject(g, gametypeMap));
|
||||
return {
|
||||
...state,
|
||||
gamesOfUser: {
|
||||
...state.gamesOfUser,
|
||||
[userName]: normalizedGames,
|
||||
},
|
||||
};
|
||||
}
|
||||
case Types.REGISTRATION_FAILED: {
|
||||
const error = action.endTime
|
||||
? normalizeBannedUserError(action.reason, action.endTime)
|
||||
: action.reason;
|
||||
return { ...state, registrationError: error };
|
||||
}
|
||||
case Types.CLEAR_REGISTRATION_ERRORS:
|
||||
return { ...state, registrationError: null };
|
||||
// Signal-only action types — no state mutation, explicit for discriminated-union exhaustiveness
|
||||
case Types.LOGIN_SUCCESSFUL:
|
||||
case Types.LOGIN_FAILED:
|
||||
case Types.CONNECTION_FAILED:
|
||||
case Types.TEST_CONNECTION_SUCCESSFUL:
|
||||
case Types.TEST_CONNECTION_FAILED:
|
||||
case Types.REGISTRATION_REQUIRES_EMAIL:
|
||||
case Types.REGISTRATION_SUCCESS:
|
||||
case Types.REGISTRATION_EMAIL_ERROR:
|
||||
case Types.REGISTRATION_PASSWORD_ERROR:
|
||||
case Types.REGISTRATION_USERNAME_ERROR:
|
||||
case Types.RESET_PASSWORD_REQUESTED:
|
||||
case Types.RESET_PASSWORD_FAILED:
|
||||
case Types.RESET_PASSWORD_CHALLENGE:
|
||||
case Types.RESET_PASSWORD_SUCCESS:
|
||||
case Types.RELOAD_CONFIG:
|
||||
case Types.SHUTDOWN_SERVER:
|
||||
case Types.UPDATE_SERVER_MESSAGE:
|
||||
case Types.ACCOUNT_PASSWORD_CHANGE:
|
||||
case Types.ADD_TO_LIST:
|
||||
case Types.REMOVE_FROM_LIST:
|
||||
case Types.GRANT_REPLAY_ACCESS:
|
||||
case Types.FORCE_ACTIVATE_USER:
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
state.gamesOfUser[userName] = normalizedGames;
|
||||
},
|
||||
|
||||
registrationFailed: (state, action: PayloadAction<{ reason: string; endTime?: number }>) => {
|
||||
const { reason, endTime } = action.payload;
|
||||
const error = endTime
|
||||
? normalizeBannedUserError(reason, endTime)
|
||||
: reason;
|
||||
state.registrationError = error;
|
||||
},
|
||||
|
||||
clearRegistrationErrors: (state) => {
|
||||
state.registrationError = null;
|
||||
},
|
||||
|
||||
accountEditChanged: (state, action: PayloadAction<{ user: Partial<Data.ServerInfo_User> }>) => {
|
||||
state.user = { ...state.user, ...action.payload.user } as Data.ServerInfo_User;
|
||||
},
|
||||
|
||||
accountImageChanged: (state, action: PayloadAction<{ user: Partial<Data.ServerInfo_User> }>) => {
|
||||
state.user = { ...state.user, ...action.payload.user } as Data.ServerInfo_User;
|
||||
},
|
||||
|
||||
// Signal-only action types — no state mutation, defined so type strings are generated
|
||||
accountAwaitingActivation: (_state, _action: PayloadAction<any>) => {},
|
||||
accountActivationFailed: (_state, _action: PayloadAction<any>) => {},
|
||||
accountActivationSuccess: (_state, _action: PayloadAction<any>) => {},
|
||||
loginSuccessful: (_state, _action: PayloadAction<any>) => {},
|
||||
loginFailed: (_state, _action: PayloadAction<any>) => {},
|
||||
connectionFailed: (_state, _action: PayloadAction<any>) => {},
|
||||
testConnectionSuccessful: (_state, _action: PayloadAction<any>) => {},
|
||||
testConnectionFailed: (_state, _action: PayloadAction<any>) => {},
|
||||
registrationRequiresEmail: (_state, _action: PayloadAction<any>) => {},
|
||||
registrationSuccess: (_state, _action: PayloadAction<any>) => {},
|
||||
registrationEmailError: (_state, _action: PayloadAction<any>) => {},
|
||||
registrationPasswordError: (_state, _action: PayloadAction<any>) => {},
|
||||
registrationUserNameError: (_state, _action: PayloadAction<any>) => {},
|
||||
resetPassword: (_state, _action: PayloadAction<any>) => {},
|
||||
resetPasswordFailed: (_state, _action: PayloadAction<any>) => {},
|
||||
resetPasswordChallenge: (_state, _action: PayloadAction<any>) => {},
|
||||
resetPasswordSuccess: (_state, _action: PayloadAction<any>) => {},
|
||||
reloadConfig: (_state, _action: PayloadAction<any>) => {},
|
||||
shutdownServer: (_state, _action: PayloadAction<any>) => {},
|
||||
updateServerMessage: (_state, _action: PayloadAction<any>) => {},
|
||||
accountPasswordChange: (_state, _action: PayloadAction<any>) => {},
|
||||
addToList: (_state, _action: PayloadAction<any>) => {},
|
||||
removeFromList: (_state, _action: PayloadAction<any>) => {},
|
||||
grantReplayAccess: (_state, _action: PayloadAction<any>) => {},
|
||||
forceActivateUser: (_state, _action: PayloadAction<any>) => {},
|
||||
},
|
||||
});
|
||||
|
||||
export const serverReducer = serverSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ describe('Selectors', () => {
|
|||
expect(Selectors.getUser(rootState(state))).toBe(user);
|
||||
});
|
||||
|
||||
it('getUsers → returns users array', () => {
|
||||
const users = [makeUser(), makeUser({ name: 'Bob' })];
|
||||
it('getUsers → returns users keyed map', () => {
|
||||
const users = { TestUser: makeUser(), Bob: makeUser({ name: 'Bob' }) };
|
||||
const state = makeServerState({ users });
|
||||
expect(Selectors.getUsers(rootState(state))).toBe(users);
|
||||
});
|
||||
|
|
@ -66,24 +66,62 @@ describe('Selectors', () => {
|
|||
expect(Selectors.getLogs(rootState(state))).toBe(logs);
|
||||
});
|
||||
|
||||
it('getBuddyList → returns buddyList', () => {
|
||||
const buddyList = [makeUser({ name: 'Carol' })];
|
||||
it('getBuddyList → returns buddyList keyed map', () => {
|
||||
const buddyList = { Carol: makeUser({ name: 'Carol' }) };
|
||||
const state = makeServerState({ buddyList });
|
||||
expect(Selectors.getBuddyList(rootState(state))).toBe(buddyList);
|
||||
});
|
||||
|
||||
it('getIgnoreList → returns ignoreList', () => {
|
||||
const ignoreList = [makeUser({ name: 'Dave' })];
|
||||
it('getIgnoreList → returns ignoreList keyed map', () => {
|
||||
const ignoreList = { Dave: makeUser({ name: 'Dave' }) };
|
||||
const state = makeServerState({ ignoreList });
|
||||
expect(Selectors.getIgnoreList(rootState(state))).toBe(ignoreList);
|
||||
});
|
||||
|
||||
it('getReplays → returns replays', () => {
|
||||
const replays = [makeReplayMatch()];
|
||||
it('getReplays → returns replays keyed map', () => {
|
||||
const replays = { 1: makeReplayMatch() };
|
||||
const state = makeServerState({ replays });
|
||||
expect(Selectors.getReplays(rootState(state))).toBe(replays);
|
||||
});
|
||||
|
||||
it('getSortedUsers → returns user array sorted by name ASC', () => {
|
||||
const users = { Zane: makeUser({ name: 'Zane' }), Alice: makeUser({ name: 'Alice' }) };
|
||||
const state = makeServerState({ users });
|
||||
const sorted = Selectors.getSortedUsers(rootState(state));
|
||||
expect(sorted[0].name).toBe('Alice');
|
||||
expect(sorted[1].name).toBe('Zane');
|
||||
});
|
||||
|
||||
it('getSortedUsers → returns EMPTY_USERS for empty map', () => {
|
||||
const state = makeServerState({ users: {} });
|
||||
const sorted = Selectors.getSortedUsers(rootState(state));
|
||||
expect(sorted).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('getSortedBuddyList → returns buddy array sorted by name ASC', () => {
|
||||
const buddyList = { Zane: makeUser({ name: 'Zane' }), Alice: makeUser({ name: 'Alice' }) };
|
||||
const state = makeServerState({ buddyList });
|
||||
const sorted = Selectors.getSortedBuddyList(rootState(state));
|
||||
expect(sorted[0].name).toBe('Alice');
|
||||
expect(sorted[1].name).toBe('Zane');
|
||||
});
|
||||
|
||||
it('getSortedIgnoreList → returns ignore array sorted by name ASC', () => {
|
||||
const ignoreList = { Zane: makeUser({ name: 'Zane' }), Alice: makeUser({ name: 'Alice' }) };
|
||||
const state = makeServerState({ ignoreList });
|
||||
const sorted = Selectors.getSortedIgnoreList(rootState(state));
|
||||
expect(sorted[0].name).toBe('Alice');
|
||||
expect(sorted[1].name).toBe('Zane');
|
||||
});
|
||||
|
||||
it('getReplaysList → returns replay array sorted by gameId ASC', () => {
|
||||
const replays = { 10: makeReplayMatch({ gameId: 10 }), 3: makeReplayMatch({ gameId: 3 }) };
|
||||
const state = makeServerState({ replays });
|
||||
const sorted = Selectors.getReplaysList(rootState(state));
|
||||
expect(sorted[0].gameId).toBe(3);
|
||||
expect(sorted[1].gameId).toBe(10);
|
||||
});
|
||||
|
||||
it('getBackendDecks → returns backendDecks', () => {
|
||||
const backendDecks = makeDeckList();
|
||||
const state = makeServerState({ backendDecks });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { App, Data } from '@app/types';
|
||||
import { SortUtil } from '../common';
|
||||
import { ServerState } from './server.interfaces';
|
||||
|
||||
interface State {
|
||||
server: ServerState
|
||||
}
|
||||
|
||||
const EMPTY_USERS: Data.ServerInfo_User[] = [];
|
||||
const EMPTY_REPLAYS: Data.ServerInfo_ReplayMatch[] = [];
|
||||
|
||||
export const Selectors = {
|
||||
getInitialized: ({ server }: State) => server.initialized,
|
||||
getMessage: ({ server }: State) => server.info.message,
|
||||
|
|
@ -13,11 +19,79 @@ export const Selectors = {
|
|||
getState: ({ server }: State) => server.status.state,
|
||||
getConnectionAttemptMade: ({ server }: State) => server.status.connectionAttemptMade,
|
||||
getUser: ({ server }: State) => server.user,
|
||||
getUsers: ({ server }: State) => server.users,
|
||||
|
||||
/** True when the server status has reached LOGGED_IN. */
|
||||
getIsConnected: createSelector(
|
||||
[({ server }: State) => server.status.state],
|
||||
(state): boolean => state === App.StatusEnum.LOGGED_IN
|
||||
),
|
||||
|
||||
/** True when the currently logged-in user has the IsModerator level flag. */
|
||||
getIsUserModerator: createSelector(
|
||||
[({ server }: State) => server.user],
|
||||
(user): boolean => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
const mask = Data.ServerInfo_User_UserLevelFlag.IsModerator;
|
||||
return (user.userLevel & mask) === mask;
|
||||
}
|
||||
),
|
||||
getLogs: ({ server }: State) => server.logs,
|
||||
getBackendDecks: ({ server }: State) => server.backendDecks,
|
||||
getRegistrationError: ({ server }: State) => server.registrationError,
|
||||
getSortUsersBy: ({ server }: State) => server.sortUsersBy,
|
||||
|
||||
/** Raw keyed maps — use the sorted-view selectors below for display. */
|
||||
getUsers: ({ server }: State) => server.users,
|
||||
getBuddyList: ({ server }: State) => server.buddyList,
|
||||
getIgnoreList: ({ server }: State) => server.ignoreList,
|
||||
getReplays: ({ server }: State) => server.replays,
|
||||
getBackendDecks: ({ server }: State) => server.backendDecks,
|
||||
getRegistrationError: ({ server }: State) => server.registrationError,
|
||||
|
||||
/**
|
||||
* Sorted array views of the keyed maps. Memoized via `createSelector` so
|
||||
* the array reference is stable until the underlying map or sort config
|
||||
* actually changes — consumers using these in `useAppSelector` won't
|
||||
* re-render unnecessarily.
|
||||
*/
|
||||
getSortedUsers: createSelector(
|
||||
[(state: State) => state.server.users, (state: State) => state.server.sortUsersBy],
|
||||
(users, sortBy): Data.ServerInfo_User[] => {
|
||||
if (!users || Object.keys(users).length === 0) {
|
||||
return EMPTY_USERS;
|
||||
}
|
||||
return SortUtil.sortedUsersByField(Object.values(users), sortBy);
|
||||
}
|
||||
),
|
||||
|
||||
getSortedBuddyList: createSelector(
|
||||
[(state: State) => state.server.buddyList, (state: State) => state.server.sortUsersBy],
|
||||
(buddyList, sortBy): Data.ServerInfo_User[] => {
|
||||
if (!buddyList || Object.keys(buddyList).length === 0) {
|
||||
return EMPTY_USERS;
|
||||
}
|
||||
return SortUtil.sortedUsersByField(Object.values(buddyList), sortBy);
|
||||
}
|
||||
),
|
||||
|
||||
getSortedIgnoreList: createSelector(
|
||||
[(state: State) => state.server.ignoreList, (state: State) => state.server.sortUsersBy],
|
||||
(ignoreList, sortBy): Data.ServerInfo_User[] => {
|
||||
if (!ignoreList || Object.keys(ignoreList).length === 0) {
|
||||
return EMPTY_USERS;
|
||||
}
|
||||
return SortUtil.sortedUsersByField(Object.values(ignoreList), sortBy);
|
||||
}
|
||||
),
|
||||
|
||||
/** Replay list as an array, ordered by gameId ascending for stable display. */
|
||||
getReplaysList: createSelector(
|
||||
[(state: State) => state.server.replays],
|
||||
(replays): Data.ServerInfo_ReplayMatch[] => {
|
||||
if (!replays || Object.keys(replays).length === 0) {
|
||||
return EMPTY_REPLAYS;
|
||||
}
|
||||
return Object.values(replays).sort((a, b) => a.gameId - b.gameId);
|
||||
}
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,78 @@
|
|||
import { serverSlice } from './server.reducer';
|
||||
|
||||
const a = serverSlice.actions;
|
||||
|
||||
export const Types = {
|
||||
INITIALIZED: '[Server] Initialized',
|
||||
CLEAR_STORE: '[Server] Clear Store',
|
||||
CONNECTION_ATTEMPTED: '[Server] Connection Attempted',
|
||||
LOGIN_SUCCESSFUL: '[Server] Login Successful',
|
||||
LOGIN_FAILED: '[Server] Login Failed',
|
||||
CONNECTION_FAILED: '[Server] Connection Failed',
|
||||
TEST_CONNECTION_SUCCESSFUL: '[Server] Test Connection Successful',
|
||||
TEST_CONNECTION_FAILED: '[Server] Test Connection Failed',
|
||||
SERVER_MESSAGE: '[Server] Server Message',
|
||||
UPDATE_BUDDY_LIST: '[Server] Update Buddy List',
|
||||
ADD_TO_BUDDY_LIST: '[Server] Add to Buddy List',
|
||||
REMOVE_FROM_BUDDY_LIST: '[Server] Remove from Buddy List',
|
||||
UPDATE_IGNORE_LIST: '[Server] Update Ignore List',
|
||||
ADD_TO_IGNORE_LIST: '[Server] Add to Ignore List',
|
||||
REMOVE_FROM_IGNORE_LIST: '[Server] Remove from Ignore List',
|
||||
UPDATE_INFO: '[Server] Update Info',
|
||||
UPDATE_STATUS: '[Server] Update Status',
|
||||
UPDATE_USER: '[Server] Update User',
|
||||
UPDATE_USERS: '[Server] Update Users',
|
||||
USER_JOINED: '[Server] User Joined',
|
||||
USER_LEFT: '[Server] User Left',
|
||||
VIEW_LOGS: '[Server] View Logs',
|
||||
CLEAR_LOGS: '[Server] Clear Logs',
|
||||
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
|
||||
REGISTRATION_SUCCESS: '[Server] Registration Success',
|
||||
REGISTRATION_FAILED: '[Server] Registration Failed',
|
||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
||||
CLEAR_REGISTRATION_ERRORS: '[Server] Clear Registration Errors',
|
||||
ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation',
|
||||
ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success',
|
||||
ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed',
|
||||
RESET_PASSWORD_REQUESTED: '[Server] Reset Password Requested',
|
||||
RESET_PASSWORD_FAILED: '[Server] Reset Password Failed',
|
||||
RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge',
|
||||
RESET_PASSWORD_SUCCESS: '[Server] Reset Password Success',
|
||||
ADJUST_MOD: '[Server] Adjust Mod',
|
||||
RELOAD_CONFIG: '[Server] Reload Config',
|
||||
SHUTDOWN_SERVER: '[Server] Shutdown Server',
|
||||
UPDATE_SERVER_MESSAGE: '[Server] Update Server Message',
|
||||
ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change',
|
||||
ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed',
|
||||
ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed',
|
||||
GET_USER_INFO: '[Server] Get User Info',
|
||||
NOTIFY_USER: '[Server] Notify User',
|
||||
SERVER_SHUTDOWN: '[Server] Server Shutdown',
|
||||
USER_MESSAGE: '[Server] User Message',
|
||||
ADD_TO_LIST: '[Server] Add To List',
|
||||
REMOVE_FROM_LIST: '[Server] Remove From List',
|
||||
BAN_FROM_SERVER: '[Server] Ban From Server',
|
||||
BAN_HISTORY: '[Server] Ban History',
|
||||
WARN_HISTORY: '[Server] Warn History',
|
||||
WARN_LIST_OPTIONS: '[Server] Warn List Options',
|
||||
WARN_USER: '[Server] Warn User',
|
||||
GRANT_REPLAY_ACCESS: '[Server] Grant Replay Access',
|
||||
FORCE_ACTIVATE_USER: '[Server] Force Activate User',
|
||||
GET_ADMIN_NOTES: '[Server] Get Admin Notes',
|
||||
UPDATE_ADMIN_NOTES: '[Server] Update Admin Notes',
|
||||
INITIALIZED: a.initialized.type,
|
||||
CLEAR_STORE: a.clearStore.type,
|
||||
CONNECTION_ATTEMPTED: a.connectionAttempted.type,
|
||||
LOGIN_SUCCESSFUL: a.loginSuccessful.type,
|
||||
LOGIN_FAILED: a.loginFailed.type,
|
||||
CONNECTION_FAILED: a.connectionFailed.type,
|
||||
TEST_CONNECTION_SUCCESSFUL: a.testConnectionSuccessful.type,
|
||||
TEST_CONNECTION_FAILED: a.testConnectionFailed.type,
|
||||
SERVER_MESSAGE: a.serverMessage.type,
|
||||
UPDATE_BUDDY_LIST: a.updateBuddyList.type,
|
||||
ADD_TO_BUDDY_LIST: a.addToBuddyList.type,
|
||||
REMOVE_FROM_BUDDY_LIST: a.removeFromBuddyList.type,
|
||||
UPDATE_IGNORE_LIST: a.updateIgnoreList.type,
|
||||
ADD_TO_IGNORE_LIST: a.addToIgnoreList.type,
|
||||
REMOVE_FROM_IGNORE_LIST: a.removeFromIgnoreList.type,
|
||||
UPDATE_INFO: a.updateInfo.type,
|
||||
UPDATE_STATUS: a.updateStatus.type,
|
||||
UPDATE_USER: a.updateUser.type,
|
||||
UPDATE_USERS: a.updateUsers.type,
|
||||
USER_JOINED: a.userJoined.type,
|
||||
USER_LEFT: a.userLeft.type,
|
||||
VIEW_LOGS: a.viewLogs.type,
|
||||
CLEAR_LOGS: a.clearLogs.type,
|
||||
REGISTRATION_REQUIRES_EMAIL: a.registrationRequiresEmail.type,
|
||||
REGISTRATION_SUCCESS: a.registrationSuccess.type,
|
||||
REGISTRATION_FAILED: a.registrationFailed.type,
|
||||
REGISTRATION_EMAIL_ERROR: a.registrationEmailError.type,
|
||||
REGISTRATION_PASSWORD_ERROR: a.registrationPasswordError.type,
|
||||
REGISTRATION_USERNAME_ERROR: a.registrationUserNameError.type,
|
||||
CLEAR_REGISTRATION_ERRORS: a.clearRegistrationErrors.type,
|
||||
ACCOUNT_AWAITING_ACTIVATION: a.accountAwaitingActivation.type,
|
||||
ACCOUNT_ACTIVATION_SUCCESS: a.accountActivationSuccess.type,
|
||||
ACCOUNT_ACTIVATION_FAILED: a.accountActivationFailed.type,
|
||||
RESET_PASSWORD_REQUESTED: a.resetPassword.type,
|
||||
RESET_PASSWORD_FAILED: a.resetPasswordFailed.type,
|
||||
RESET_PASSWORD_CHALLENGE: a.resetPasswordChallenge.type,
|
||||
RESET_PASSWORD_SUCCESS: a.resetPasswordSuccess.type,
|
||||
ADJUST_MOD: a.adjustMod.type,
|
||||
RELOAD_CONFIG: a.reloadConfig.type,
|
||||
SHUTDOWN_SERVER: a.shutdownServer.type,
|
||||
UPDATE_SERVER_MESSAGE: a.updateServerMessage.type,
|
||||
ACCOUNT_PASSWORD_CHANGE: a.accountPasswordChange.type,
|
||||
ACCOUNT_EDIT_CHANGED: a.accountEditChanged.type,
|
||||
ACCOUNT_IMAGE_CHANGED: a.accountImageChanged.type,
|
||||
GET_USER_INFO: a.getUserInfo.type,
|
||||
NOTIFY_USER: a.notifyUser.type,
|
||||
SERVER_SHUTDOWN: a.serverShutdown.type,
|
||||
USER_MESSAGE: a.userMessage.type,
|
||||
ADD_TO_LIST: a.addToList.type,
|
||||
REMOVE_FROM_LIST: a.removeFromList.type,
|
||||
BAN_FROM_SERVER: a.banFromServer.type,
|
||||
BAN_HISTORY: a.banHistory.type,
|
||||
WARN_HISTORY: a.warnHistory.type,
|
||||
WARN_LIST_OPTIONS: a.warnListOptions.type,
|
||||
WARN_USER: a.warnUser.type,
|
||||
GRANT_REPLAY_ACCESS: a.grantReplayAccess.type,
|
||||
FORCE_ACTIVATE_USER: a.forceActivateUser.type,
|
||||
GET_ADMIN_NOTES: a.getAdminNotes.type,
|
||||
UPDATE_ADMIN_NOTES: a.updateAdminNotes.type,
|
||||
// Replay
|
||||
REPLAY_LIST: '[Server] Replay List',
|
||||
REPLAY_ADDED: '[Server] Replay Added',
|
||||
REPLAY_MODIFY_MATCH: '[Server] Replay Modify Match',
|
||||
REPLAY_DELETE_MATCH: '[Server] Replay Delete Match',
|
||||
REPLAY_LIST: a.replayList.type,
|
||||
REPLAY_ADDED: a.replayAdded.type,
|
||||
REPLAY_MODIFY_MATCH: a.replayModifyMatch.type,
|
||||
REPLAY_DELETE_MATCH: a.replayDeleteMatch.type,
|
||||
// Deck Storage
|
||||
BACKEND_DECKS: '[Server] Backend Decks',
|
||||
DECK_NEW_DIR: '[Server] Deck New Dir',
|
||||
DECK_DEL_DIR: '[Server] Deck Del Dir',
|
||||
DECK_UPLOAD: '[Server] Deck Upload',
|
||||
DECK_DELETE: '[Server] Deck Delete',
|
||||
BACKEND_DECKS: a.backendDecks.type,
|
||||
DECK_NEW_DIR: a.deckNewDir.type,
|
||||
DECK_DEL_DIR: a.deckDelDir.type,
|
||||
DECK_UPLOAD: a.deckUpload.type,
|
||||
DECK_DELETE: a.deckDelete.type,
|
||||
// User games
|
||||
GAMES_OF_USER: '[Server] Games Of User',
|
||||
GAMES_OF_USER: a.gamesOfUser.type,
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue