mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
complete unit testing of redux and api layers
This commit is contained in:
parent
367852866f
commit
3001925430
19 changed files with 2808 additions and 5 deletions
48
webclient/src/api/AdminService.spec.ts
Normal file
48
webclient/src/api/AdminService.spec.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
jest.mock('websocket', () => ({
|
||||
AdminCommands: {
|
||||
adjustMod: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
shutdownServer: jest.fn(),
|
||||
updateServerMessage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { AdminService } from './AdminService';
|
||||
import { AdminCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('AdminService', () => {
|
||||
describe('adjustMod', () => {
|
||||
it('delegates to AdminCommands.adjustMod with all arguments', () => {
|
||||
AdminService.adjustMod('alice', true, false);
|
||||
expect(AdminCommands.adjustMod).toHaveBeenCalledWith('alice', true, false);
|
||||
});
|
||||
|
||||
it('delegates with optional arguments omitted', () => {
|
||||
AdminService.adjustMod('alice');
|
||||
expect(AdminCommands.adjustMod).toHaveBeenCalledWith('alice', undefined, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadConfig', () => {
|
||||
it('delegates to AdminCommands.reloadConfig', () => {
|
||||
AdminService.reloadConfig();
|
||||
expect(AdminCommands.reloadConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdownServer', () => {
|
||||
it('delegates to AdminCommands.shutdownServer', () => {
|
||||
AdminService.shutdownServer('maintenance', 10);
|
||||
expect(AdminCommands.shutdownServer).toHaveBeenCalledWith('maintenance', 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateServerMessage', () => {
|
||||
it('delegates to AdminCommands.updateServerMessage', () => {
|
||||
AdminService.updateServerMessage();
|
||||
expect(AdminCommands.updateServerMessage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
145
webclient/src/api/AuthenticationService.spec.ts
Normal file
145
webclient/src/api/AuthenticationService.spec.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
jest.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
},
|
||||
webClient: {
|
||||
connectionAttemptMade: false,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('websocket/services/ProtoController', () => ({
|
||||
ProtoController: {
|
||||
root: {
|
||||
ServerInfo_User: {
|
||||
UserLevelFlag: {
|
||||
IsModerator: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { AuthenticationService } from './AuthenticationService';
|
||||
import { SessionCommands, webClient } from 'websocket';
|
||||
import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types';
|
||||
|
||||
const testOptions: WebSocketConnectOptions = { host: 'localhost', port: '4748', userName: 'user', password: 'pw' };
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('AuthenticationService', () => {
|
||||
describe('login', () => {
|
||||
it('calls SessionCommands.connect with LOGIN reason', () => {
|
||||
AuthenticationService.login(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testConnection', () => {
|
||||
it('calls SessionCommands.connect with TEST_CONNECTION reason', () => {
|
||||
AuthenticationService.testConnection(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.TEST_CONNECTION);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('calls SessionCommands.connect with REGISTER reason', () => {
|
||||
AuthenticationService.register(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.REGISTER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateAccount', () => {
|
||||
it('calls SessionCommands.connect with ACTIVATE_ACCOUNT reason', () => {
|
||||
AuthenticationService.activateAccount(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPasswordRequest', () => {
|
||||
it('calls SessionCommands.connect with PASSWORD_RESET_REQUEST reason', () => {
|
||||
AuthenticationService.resetPasswordRequest(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPasswordChallenge', () => {
|
||||
it('calls SessionCommands.connect with PASSWORD_RESET_CHALLENGE reason', () => {
|
||||
AuthenticationService.resetPasswordChallenge(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('calls SessionCommands.connect with PASSWORD_RESET reason', () => {
|
||||
AuthenticationService.resetPassword(testOptions);
|
||||
expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('delegates to SessionCommands.disconnect', () => {
|
||||
AuthenticationService.disconnect();
|
||||
expect(SessionCommands.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnected', () => {
|
||||
it('returns true when state is LOGGED_IN', () => {
|
||||
expect(AuthenticationService.isConnected(StatusEnum.LOGGED_IN)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when state is DISCONNECTED', () => {
|
||||
expect(AuthenticationService.isConnected(StatusEnum.DISCONNECTED)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when state is CONNECTING', () => {
|
||||
expect(AuthenticationService.isConnected(StatusEnum.CONNECTING)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when state is CONNECTED', () => {
|
||||
expect(AuthenticationService.isConnected(StatusEnum.CONNECTED)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when state is LOGGING_IN', () => {
|
||||
expect(AuthenticationService.isConnected(StatusEnum.LOGGING_IN)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModerator', () => {
|
||||
it('returns true when userLevel has the IsModerator bit set', () => {
|
||||
expect(AuthenticationService.isModerator({ userLevel: 4 } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when userLevel has IsModerator and other bits set', () => {
|
||||
expect(AuthenticationService.isModerator({ userLevel: 7 } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when userLevel does not have the IsModerator bit', () => {
|
||||
expect(AuthenticationService.isModerator({ userLevel: 1 } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for admin-only userLevel without moderator bit', () => {
|
||||
expect(AuthenticationService.isModerator({ userLevel: 8 } as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAdmin', () => {
|
||||
it('returns undefined (not yet implemented)', () => {
|
||||
expect(AuthenticationService.isAdmin()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectionAttemptMade', () => {
|
||||
it('returns webClient.connectionAttemptMade when false', () => {
|
||||
(webClient as any).connectionAttemptMade = false;
|
||||
expect(AuthenticationService.connectionAttemptMade()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns webClient.connectionAttemptMade when true', () => {
|
||||
(webClient as any).connectionAttemptMade = true;
|
||||
expect(AuthenticationService.connectionAttemptMade()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
75
webclient/src/api/ModeratorService.spec.ts
Normal file
75
webclient/src/api/ModeratorService.spec.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
jest.mock('websocket', () => ({
|
||||
ModeratorCommands: {
|
||||
banFromServer: jest.fn(),
|
||||
getBanHistory: jest.fn(),
|
||||
getWarnHistory: jest.fn(),
|
||||
getWarnList: jest.fn(),
|
||||
viewLogHistory: jest.fn(),
|
||||
warnUser: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { ModeratorService } from './ModeratorService';
|
||||
import { ModeratorCommands } from 'websocket';
|
||||
import { LogFilters } from 'types';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('ModeratorService', () => {
|
||||
describe('banFromServer', () => {
|
||||
it('delegates to ModeratorCommands.banFromServer with all arguments', () => {
|
||||
ModeratorService.banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible reason', 'cid', 1);
|
||||
expect(ModeratorCommands.banFromServer).toHaveBeenCalledWith(
|
||||
30, 'alice', '1.2.3.4', 'reason', 'visible reason', 'cid', 1
|
||||
);
|
||||
});
|
||||
|
||||
it('delegates with only required argument', () => {
|
||||
ModeratorService.banFromServer(60);
|
||||
expect(ModeratorCommands.banFromServer).toHaveBeenCalledWith(
|
||||
60, undefined, undefined, undefined, undefined, undefined, undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBanHistory', () => {
|
||||
it('delegates to ModeratorCommands.getBanHistory', () => {
|
||||
ModeratorService.getBanHistory('alice');
|
||||
expect(ModeratorCommands.getBanHistory).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWarnHistory', () => {
|
||||
it('delegates to ModeratorCommands.getWarnHistory', () => {
|
||||
ModeratorService.getWarnHistory('alice');
|
||||
expect(ModeratorCommands.getWarnHistory).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWarnList', () => {
|
||||
it('delegates to ModeratorCommands.getWarnList', () => {
|
||||
ModeratorService.getWarnList('mod1', 'alice', 'cid123');
|
||||
expect(ModeratorCommands.getWarnList).toHaveBeenCalledWith('mod1', 'alice', 'cid123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewLogHistory', () => {
|
||||
it('delegates to ModeratorCommands.viewLogHistory', () => {
|
||||
const filters: LogFilters = { dateRange: 7, userName: 'alice' };
|
||||
ModeratorService.viewLogHistory(filters);
|
||||
expect(ModeratorCommands.viewLogHistory).toHaveBeenCalledWith(filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('warnUser', () => {
|
||||
it('delegates to ModeratorCommands.warnUser with all arguments', () => {
|
||||
ModeratorService.warnUser('alice', 'spamming', 'cid', 5);
|
||||
expect(ModeratorCommands.warnUser).toHaveBeenCalledWith('alice', 'spamming', 'cid', 5);
|
||||
});
|
||||
|
||||
it('delegates with only required arguments', () => {
|
||||
ModeratorService.warnUser('alice', 'spamming');
|
||||
expect(ModeratorCommands.warnUser).toHaveBeenCalledWith('alice', 'spamming', undefined, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
37
webclient/src/api/RoomsService.spec.ts
Normal file
37
webclient/src/api/RoomsService.spec.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
jest.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
joinRoom: jest.fn(),
|
||||
},
|
||||
RoomCommands: {
|
||||
leaveRoom: jest.fn(),
|
||||
roomSay: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { RoomsService } from './RoomsService';
|
||||
import { RoomCommands, SessionCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('RoomsService', () => {
|
||||
describe('joinRoom', () => {
|
||||
it('delegates to SessionCommands.joinRoom', () => {
|
||||
RoomsService.joinRoom(42);
|
||||
expect(SessionCommands.joinRoom).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaveRoom', () => {
|
||||
it('delegates to RoomCommands.leaveRoom', () => {
|
||||
RoomsService.leaveRoom(42);
|
||||
expect(RoomCommands.leaveRoom).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roomSay', () => {
|
||||
it('delegates to RoomCommands.roomSay', () => {
|
||||
RoomsService.roomSay(42, 'hello room');
|
||||
expect(RoomCommands.roomSay).toHaveBeenCalledWith(42, 'hello room');
|
||||
});
|
||||
});
|
||||
});
|
||||
102
webclient/src/api/SessionService.spec.ts
Normal file
102
webclient/src/api/SessionService.spec.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
jest.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
addToBuddyList: jest.fn(),
|
||||
removeFromBuddyList: jest.fn(),
|
||||
addToIgnoreList: jest.fn(),
|
||||
removeFromIgnoreList: jest.fn(),
|
||||
accountPassword: jest.fn(),
|
||||
accountEdit: jest.fn(),
|
||||
accountImage: jest.fn(),
|
||||
message: jest.fn(),
|
||||
getUserInfo: jest.fn(),
|
||||
getGamesOfUser: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { SessionService } from './SessionService';
|
||||
import { SessionCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('SessionService', () => {
|
||||
describe('addToBuddyList', () => {
|
||||
it('delegates to SessionCommands.addToBuddyList', () => {
|
||||
SessionService.addToBuddyList('alice');
|
||||
expect(SessionCommands.addToBuddyList).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromBuddyList', () => {
|
||||
it('delegates to SessionCommands.removeFromBuddyList', () => {
|
||||
SessionService.removeFromBuddyList('alice');
|
||||
expect(SessionCommands.removeFromBuddyList).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToIgnoreList', () => {
|
||||
it('delegates to SessionCommands.addToIgnoreList', () => {
|
||||
SessionService.addToIgnoreList('bob');
|
||||
expect(SessionCommands.addToIgnoreList).toHaveBeenCalledWith('bob');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromIgnoreList', () => {
|
||||
it('delegates to SessionCommands.removeFromIgnoreList', () => {
|
||||
SessionService.removeFromIgnoreList('bob');
|
||||
expect(SessionCommands.removeFromIgnoreList).toHaveBeenCalledWith('bob');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeAccountPassword', () => {
|
||||
it('delegates to SessionCommands.accountPassword with all arguments', () => {
|
||||
SessionService.changeAccountPassword('oldPw', 'newPw', 'hashedPw');
|
||||
expect(SessionCommands.accountPassword).toHaveBeenCalledWith('oldPw', 'newPw', 'hashedPw');
|
||||
});
|
||||
|
||||
it('delegates without hashedNewPassword when omitted', () => {
|
||||
SessionService.changeAccountPassword('oldPw', 'newPw');
|
||||
expect(SessionCommands.accountPassword).toHaveBeenCalledWith('oldPw', 'newPw', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeAccountDetails', () => {
|
||||
it('delegates to SessionCommands.accountEdit with all arguments', () => {
|
||||
SessionService.changeAccountDetails('pw', 'Alice', 'alice@example.com', 'US');
|
||||
expect(SessionCommands.accountEdit).toHaveBeenCalledWith('pw', 'Alice', 'alice@example.com', 'US');
|
||||
});
|
||||
|
||||
it('delegates with only required argument', () => {
|
||||
SessionService.changeAccountDetails('pw');
|
||||
expect(SessionCommands.accountEdit).toHaveBeenCalledWith('pw', undefined, undefined, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeAccountImage', () => {
|
||||
it('delegates to SessionCommands.accountImage', () => {
|
||||
const image = new Uint8Array([1, 2, 3]);
|
||||
SessionService.changeAccountImage(image);
|
||||
expect(SessionCommands.accountImage).toHaveBeenCalledWith(image);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendDirectMessage', () => {
|
||||
it('delegates to SessionCommands.message', () => {
|
||||
SessionService.sendDirectMessage('alice', 'hello');
|
||||
expect(SessionCommands.message).toHaveBeenCalledWith('alice', 'hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
it('delegates to SessionCommands.getUserInfo', () => {
|
||||
SessionService.getUserInfo('alice');
|
||||
expect(SessionCommands.getUserInfo).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserGames', () => {
|
||||
it('delegates to SessionCommands.getGamesOfUser', () => {
|
||||
SessionService.getUserGames('alice');
|
||||
expect(SessionCommands.getGamesOfUser).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
});
|
||||
40
webclient/src/store/actions/actionReducer.spec.ts
Normal file
40
webclient/src/store/actions/actionReducer.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { actionReducer } from './actionReducer';
|
||||
|
||||
describe('actionReducer', () => {
|
||||
it('spreads the init action onto state and starts count at 1', () => {
|
||||
const result = actionReducer(undefined, { type: '@@INIT' });
|
||||
// actionReducer always spreads the action, so type reflects the dispatched action
|
||||
expect(result.type).toBe('@@INIT');
|
||||
expect(result.payload).toBeNull();
|
||||
expect(result.meta).toBeNull();
|
||||
expect(result.error).toBe(false);
|
||||
expect(result.count).toBe(1);
|
||||
});
|
||||
|
||||
it('spreads action onto state and increments count', () => {
|
||||
const result = actionReducer(undefined, { type: 'MY_ACTION', payload: { id: 1 } });
|
||||
expect(result.type).toBe('MY_ACTION');
|
||||
expect(result.payload).toEqual({ id: 1 });
|
||||
expect(result.count).toBe(1);
|
||||
});
|
||||
|
||||
it('increments count on each dispatch', () => {
|
||||
const state1 = actionReducer(undefined, { type: 'A' });
|
||||
const state2 = actionReducer(state1, { type: 'B' });
|
||||
const state3 = actionReducer(state2, { type: 'C' });
|
||||
expect(state3.count).toBe(3);
|
||||
});
|
||||
|
||||
it('preserves existing state fields not overridden by action', () => {
|
||||
const initial = actionReducer(undefined, { type: 'FIRST', payload: 'original' });
|
||||
const result = actionReducer(initial, { type: 'SECOND' });
|
||||
expect(result.type).toBe('SECOND');
|
||||
expect(result.count).toBe(2);
|
||||
});
|
||||
|
||||
it('spreads action.meta and action.error from action onto state', () => {
|
||||
const result = actionReducer(undefined, { type: 'ERR', meta: { source: 'api' }, error: true });
|
||||
expect(result.meta).toEqual({ source: 'api' });
|
||||
expect(result.error).toBe(true);
|
||||
});
|
||||
});
|
||||
178
webclient/src/store/common/SortUtil.spec.ts
Normal file
178
webclient/src/store/common/SortUtil.spec.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { SortDirection } from 'types';
|
||||
import SortUtil from './SortUtil';
|
||||
|
||||
// ── sortByField ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('sortByField', () => {
|
||||
it('sorts string field ASC alphabetically', () => {
|
||||
const arr = [{ name: 'Zane' }, { name: 'Alice' }, { name: 'Bob' }];
|
||||
SortUtil.sortByField(arr, { field: 'name', order: SortDirection.ASC });
|
||||
expect(arr.map(x => x.name)).toEqual(['Alice', 'Bob', 'Zane']);
|
||||
});
|
||||
|
||||
it('sorts string field DESC reverse-alphabetically', () => {
|
||||
const arr = [{ name: 'Alice' }, { name: 'Zane' }, { name: 'Bob' }];
|
||||
SortUtil.sortByField(arr, { field: 'name', order: SortDirection.DESC });
|
||||
expect(arr.map(x => x.name)).toEqual(['Zane', 'Bob', 'Alice']);
|
||||
});
|
||||
|
||||
it('sorts number field ASC', () => {
|
||||
const arr = [{ score: 30 }, { score: 10 }, { score: 20 }];
|
||||
SortUtil.sortByField(arr, { field: 'score', order: SortDirection.ASC });
|
||||
expect(arr.map(x => x.score)).toEqual([10, 20, 30]);
|
||||
});
|
||||
|
||||
it('sorts number field DESC', () => {
|
||||
const arr = [{ score: 10 }, { score: 30 }, { score: 20 }];
|
||||
SortUtil.sortByField(arr, { field: 'score', order: SortDirection.DESC });
|
||||
expect(arr.map(x => x.score)).toEqual([30, 20, 10]);
|
||||
});
|
||||
|
||||
it('no-ops on empty array without error', () => {
|
||||
expect(() => SortUtil.sortByField([], { field: 'name', order: SortDirection.ASC })).not.toThrow();
|
||||
});
|
||||
|
||||
it('sorts with nested dot-notation field', () => {
|
||||
const arr = [{ meta: { rank: 3 } }, { meta: { rank: 1 } }, { meta: { rank: 2 } }];
|
||||
SortUtil.sortByField(arr, { field: 'meta.rank', order: SortDirection.ASC });
|
||||
expect(arr.map(x => x.meta.rank)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('throws when field resolves to a non-string, non-number value', () => {
|
||||
const arr = [{ data: {} }, { data: {} }];
|
||||
expect(() => SortUtil.sortByField(arr, { field: 'data', order: SortDirection.ASC })).toThrow(
|
||||
'SortField must resolve to either a string or number'
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts empty-string values to the bottom when sorting ASC', () => {
|
||||
const arr = [{ name: '' }, { name: 'Alice' }, { name: '' }];
|
||||
SortUtil.sortByField(arr, { field: 'name', order: SortDirection.ASC });
|
||||
expect(arr[0].name).toBe('Alice');
|
||||
expect(arr[1].name).toBe('');
|
||||
expect(arr[2].name).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ── sortByFields ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('sortByFields', () => {
|
||||
it('sorts by the first key when all items have distinct first-key values', () => {
|
||||
const arr = [
|
||||
{ group: 'C', name: 'Zane' },
|
||||
{ group: 'A', name: 'Bob' },
|
||||
{ group: 'B', name: 'Alice' },
|
||||
];
|
||||
SortUtil.sortByFields(arr, [
|
||||
{ field: 'group', order: SortDirection.ASC },
|
||||
{ field: 'name', order: SortDirection.ASC },
|
||||
]);
|
||||
expect(arr.map(x => x.group)).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('breaks ties on primary key using secondary key', () => {
|
||||
const arr = [
|
||||
{ group: 'A', name: 'Zane' },
|
||||
{ group: 'A', name: 'Alice' },
|
||||
{ group: 'B', name: 'Bob' },
|
||||
];
|
||||
SortUtil.sortByFields(arr, [
|
||||
{ field: 'group', order: SortDirection.ASC },
|
||||
{ field: 'name', order: SortDirection.ASC },
|
||||
]);
|
||||
expect(arr[0]).toEqual({ group: 'A', name: 'Alice' });
|
||||
expect(arr[1]).toEqual({ group: 'A', name: 'Zane' });
|
||||
expect(arr[2]).toEqual({ group: 'B', name: 'Bob' });
|
||||
});
|
||||
|
||||
it('no-ops on empty array', () => {
|
||||
expect(() =>
|
||||
SortUtil.sortByFields([], [{ field: 'name', order: SortDirection.ASC }])
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('sorts by number field', () => {
|
||||
const arr = [{ score: 3 }, { score: 1 }, { score: 2 }];
|
||||
SortUtil.sortByFields(arr, [{ field: 'score', order: SortDirection.ASC }]);
|
||||
expect(arr.map(x => x.score)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('returns 0 when all items tie on every sort key', () => {
|
||||
const arr = [{ score: 5 }, { score: 5 }];
|
||||
expect(() =>
|
||||
SortUtil.sortByFields(arr, [{ field: 'score', order: SortDirection.ASC }])
|
||||
).not.toThrow();
|
||||
expect(arr).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('throws when field resolves to a non-string, non-number value', () => {
|
||||
const arr = [{ data: {} }, { data: {} }];
|
||||
expect(() =>
|
||||
SortUtil.sortByFields(arr, [{ field: 'data', order: SortDirection.ASC }])
|
||||
).toThrow('SortField must resolve to either a string or number');
|
||||
});
|
||||
});
|
||||
|
||||
// ── sortUsersByField ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('sortUsersByField', () => {
|
||||
it('sorts by userLevel DESC first, then name ASC', () => {
|
||||
const users = [
|
||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 },
|
||||
{ name: 'Bob', userLevel: 8, accountageSecs: 0, privlevel: 0 },
|
||||
{ name: 'Carol', userLevel: 1, accountageSecs: 0, privlevel: 0 },
|
||||
];
|
||||
SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC });
|
||||
expect(users[0].name).toBe('Bob');
|
||||
expect(users[1].name).toBe('Alice');
|
||||
expect(users[2].name).toBe('Carol');
|
||||
});
|
||||
|
||||
it('no-ops on empty array', () => {
|
||||
expect(() =>
|
||||
SortUtil.sortUsersByField([], { field: 'name', order: SortDirection.ASC })
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('returns 0 (stable) when two users tie on both userLevel and name', () => {
|
||||
const users = [
|
||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 },
|
||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 },
|
||||
];
|
||||
expect(() =>
|
||||
SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC })
|
||||
).not.toThrow();
|
||||
expect(users).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── toggleSortBy ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('toggleSortBy', () => {
|
||||
it('same field + ASC → returns DESC', () => {
|
||||
const result = SortUtil.toggleSortBy('name', { field: 'name', order: SortDirection.ASC });
|
||||
expect(result).toEqual({ field: 'name', order: SortDirection.DESC });
|
||||
});
|
||||
|
||||
it('same field + DESC → returns ASC', () => {
|
||||
const result = SortUtil.toggleSortBy('name', { field: 'name', order: SortDirection.DESC });
|
||||
expect(result).toEqual({ field: 'name', order: SortDirection.ASC });
|
||||
});
|
||||
|
||||
it('different field → returns ASC regardless of current order', () => {
|
||||
const result = SortUtil.toggleSortBy('score', { field: 'name', order: SortDirection.DESC });
|
||||
expect(result).toEqual({ field: 'score', order: SortDirection.ASC });
|
||||
});
|
||||
});
|
||||
|
||||
// ── resolveFieldChain with numeric index ─────────────────────────────────────
|
||||
|
||||
describe('resolveFieldChain via sortByField (numeric index)', () => {
|
||||
it('resolves numeric index in dot-notation chain', () => {
|
||||
const arr = [{ items: ['c', 'b', 'a'] }, { items: ['z', 'y', 'x'] }];
|
||||
// Sort by items.0 which is the first element of the items array
|
||||
SortUtil.sortByField(arr, { field: 'items.0', order: SortDirection.ASC });
|
||||
expect(arr[0].items[0]).toBe('c');
|
||||
expect(arr[1].items[0]).toBe('z');
|
||||
});
|
||||
});
|
||||
|
|
@ -35,17 +35,15 @@ export default class SortUtil {
|
|||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldType === 'number') {
|
||||
} else if (fieldType === 'number') {
|
||||
const result = SortUtil.numberComparator(a, b, sortBy);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
throw new Error('SortField must resolve to either a string or number');
|
||||
}
|
||||
|
||||
throw new Error('SortField must resolve to either a string or number');
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -383,6 +383,31 @@ describe('2C: CARD_MOVED', () => {
|
|||
expect(moved.name).toBe('New Name');
|
||||
expect(moved.providerId).toBe('new-prov');
|
||||
});
|
||||
|
||||
it('CARD_MOVED → returns newState (card removed from source) when targetZone does not exist on player', () => {
|
||||
const { state } = stateWithCard();
|
||||
const result = gamesReducer(state, {
|
||||
type: Types.CARD_MOVED,
|
||||
gameId: 1,
|
||||
playerId: 1,
|
||||
data: {
|
||||
cardId: 10,
|
||||
cardName: '',
|
||||
startPlayerId: 1,
|
||||
startZone: 'hand',
|
||||
position: -1,
|
||||
targetPlayerId: 1,
|
||||
targetZone: 'nonexistent',
|
||||
x: 0,
|
||||
y: 0,
|
||||
newCardId: -1,
|
||||
faceDown: false,
|
||||
newCardProviderId: '',
|
||||
},
|
||||
});
|
||||
expect(result.games[1].players[1].zones['hand'].cards).toHaveLength(0);
|
||||
expect(result.games[1].players[1].zones['nonexistent']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 2D: Card mutations ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
82
webclient/src/store/rooms/__mocks__/rooms-fixtures.ts
Normal file
82
webclient/src/store/rooms/__mocks__/rooms-fixtures.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import {
|
||||
Game,
|
||||
GameSortField,
|
||||
Room,
|
||||
SortDirection,
|
||||
User,
|
||||
UserPrivLevel,
|
||||
UserSortField,
|
||||
} from 'types';
|
||||
import { Message, RoomsState } from '../rooms.interfaces';
|
||||
|
||||
export function makeUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
name: 'TestUser',
|
||||
accountageSecs: 0,
|
||||
privlevel: UserPrivLevel.NONE,
|
||||
userLevel: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeRoom(overrides: Partial<Room> = {}): Room {
|
||||
return {
|
||||
roomId: 1,
|
||||
name: 'Test Room',
|
||||
description: '',
|
||||
gameCount: 0,
|
||||
gameList: [],
|
||||
gametypeList: [],
|
||||
gametypeMap: {},
|
||||
autoJoin: false,
|
||||
permissionlevel: 0 as any,
|
||||
playerCount: 0,
|
||||
privilegelevel: 0 as any,
|
||||
userList: [],
|
||||
order: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeGame(overrides: Partial<Game & { startTime: number }> = {}): Game & { startTime: number } {
|
||||
return {
|
||||
gameId: 1,
|
||||
roomId: 1,
|
||||
description: 'Test Game',
|
||||
gameType: '',
|
||||
gameTypes: [],
|
||||
started: false,
|
||||
startTime: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
message: 'hello',
|
||||
messageType: 0,
|
||||
timeReceived: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeRoomsState(overrides: Partial<RoomsState> = {}): RoomsState {
|
||||
return {
|
||||
rooms: {
|
||||
1: makeRoom({ roomId: 1 }),
|
||||
},
|
||||
games: {},
|
||||
joinedRoomIds: {},
|
||||
joinedGameIds: {},
|
||||
messages: {},
|
||||
sortGamesBy: {
|
||||
field: GameSortField.START_TIME,
|
||||
order: SortDirection.DESC,
|
||||
},
|
||||
sortUsersBy: {
|
||||
field: UserSortField.NAME,
|
||||
order: SortDirection.ASC,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
69
webclient/src/store/rooms/rooms.actions.spec.ts
Normal file
69
webclient/src/store/rooms/rooms.actions.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { Actions } from './rooms.actions';
|
||||
import { Types } from './rooms.types';
|
||||
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
|
||||
import { GameSortField, SortDirection } from 'types';
|
||||
|
||||
describe('Actions', () => {
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
});
|
||||
|
||||
it('updateRooms', () => {
|
||||
const rooms = [makeRoom()];
|
||||
expect(Actions.updateRooms(rooms)).toEqual({ type: Types.UPDATE_ROOMS, rooms });
|
||||
});
|
||||
|
||||
it('joinRoom', () => {
|
||||
const roomInfo = makeRoom({ roomId: 2 });
|
||||
expect(Actions.joinRoom(roomInfo)).toEqual({ type: Types.JOIN_ROOM, roomInfo });
|
||||
});
|
||||
|
||||
it('leaveRoom', () => {
|
||||
expect(Actions.leaveRoom(3)).toEqual({ type: Types.LEAVE_ROOM, roomId: 3 });
|
||||
});
|
||||
|
||||
it('addMessage', () => {
|
||||
const message = makeMessage();
|
||||
expect(Actions.addMessage(1, message)).toEqual({ type: Types.ADD_MESSAGE, roomId: 1, message });
|
||||
});
|
||||
|
||||
it('updateGames', () => {
|
||||
const games = [makeGame()];
|
||||
expect(Actions.updateGames(1, games)).toEqual({ type: Types.UPDATE_GAMES, roomId: 1, games });
|
||||
});
|
||||
|
||||
it('userJoined', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.userJoined(1, user)).toEqual({ type: Types.USER_JOINED, roomId: 1, user });
|
||||
});
|
||||
|
||||
it('userLeft', () => {
|
||||
expect(Actions.userLeft(1, 'Alice')).toEqual({ type: Types.USER_LEFT, roomId: 1, name: 'Alice' });
|
||||
});
|
||||
|
||||
it('sortGames', () => {
|
||||
expect(Actions.sortGames(1, GameSortField.START_TIME, SortDirection.ASC)).toEqual({
|
||||
type: Types.SORT_GAMES,
|
||||
roomId: 1,
|
||||
field: GameSortField.START_TIME,
|
||||
order: SortDirection.ASC,
|
||||
});
|
||||
});
|
||||
|
||||
it('removeMessages', () => {
|
||||
expect(Actions.removeMessages(1, 'Alice', 3)).toEqual({
|
||||
type: Types.REMOVE_MESSAGES,
|
||||
roomId: 1,
|
||||
name: 'Alice',
|
||||
amount: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('gameCreated', () => {
|
||||
expect(Actions.gameCreated(2)).toEqual({ type: Types.GAME_CREATED, roomId: 2 });
|
||||
});
|
||||
|
||||
it('joinedGame', () => {
|
||||
expect(Actions.joinedGame(1, 5)).toEqual({ type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
|
||||
});
|
||||
});
|
||||
90
webclient/src/store/rooms/rooms.dispatch.spec.ts
Normal file
90
webclient/src/store/rooms/rooms.dispatch.spec.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } }));
|
||||
jest.mock('redux-form', () => ({
|
||||
reset: jest.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
|
||||
}));
|
||||
|
||||
import { store } from 'store/store';
|
||||
import { reset } from 'redux-form';
|
||||
import { Actions } from './rooms.actions';
|
||||
import { Dispatch } from './rooms.dispatch';
|
||||
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
|
||||
import { GameSortField, SortDirection } from 'types';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('Dispatch', () => {
|
||||
it('clearStore dispatches Actions.clearStore()', () => {
|
||||
Dispatch.clearStore();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.clearStore());
|
||||
});
|
||||
|
||||
it('updateRooms dispatches Actions.updateRooms()', () => {
|
||||
const rooms = [makeRoom()];
|
||||
Dispatch.updateRooms(rooms);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateRooms(rooms));
|
||||
});
|
||||
|
||||
it('joinRoom dispatches Actions.joinRoom()', () => {
|
||||
const roomInfo = makeRoom({ roomId: 2 });
|
||||
Dispatch.joinRoom(roomInfo);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.joinRoom(roomInfo));
|
||||
});
|
||||
|
||||
it('leaveRoom dispatches Actions.leaveRoom()', () => {
|
||||
Dispatch.leaveRoom(3);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.leaveRoom(3));
|
||||
});
|
||||
|
||||
it('addMessage with message.name falsy → dispatches only Actions.addMessage()', () => {
|
||||
const message = { ...makeMessage(), name: undefined };
|
||||
Dispatch.addMessage(1, message);
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
||||
});
|
||||
|
||||
it('addMessage with message.name truthy → dispatches reset("sayMessage") then Actions.addMessage()', () => {
|
||||
const message = { ...makeMessage(), name: 'Alice' };
|
||||
Dispatch.addMessage(1, message);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('sayMessage'));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addMessage(1, message));
|
||||
});
|
||||
|
||||
it('updateGames dispatches Actions.updateGames()', () => {
|
||||
const games = [makeGame()];
|
||||
Dispatch.updateGames(1, games);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateGames(1, games));
|
||||
});
|
||||
|
||||
it('userJoined dispatches Actions.userJoined()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.userJoined(1, user);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userJoined(1, user));
|
||||
});
|
||||
|
||||
it('userLeft dispatches Actions.userLeft()', () => {
|
||||
Dispatch.userLeft(1, 'Alice');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userLeft(1, 'Alice'));
|
||||
});
|
||||
|
||||
it('sortGames dispatches Actions.sortGames()', () => {
|
||||
Dispatch.sortGames(1, GameSortField.START_TIME, SortDirection.ASC);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
Actions.sortGames(1, GameSortField.START_TIME, SortDirection.ASC)
|
||||
);
|
||||
});
|
||||
|
||||
it('removeMessages dispatches Actions.removeMessages()', () => {
|
||||
Dispatch.removeMessages(1, 'Alice', 5);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.removeMessages(1, 'Alice', 5));
|
||||
});
|
||||
|
||||
it('gameCreated dispatches Actions.gameCreated()', () => {
|
||||
Dispatch.gameCreated(2);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameCreated(2));
|
||||
});
|
||||
|
||||
it('joinedGame dispatches Actions.joinedGame()', () => {
|
||||
Dispatch.joinedGame(1, 5);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.joinedGame(1, 5));
|
||||
});
|
||||
});
|
||||
285
webclient/src/store/rooms/rooms.reducer.spec.ts
Normal file
285
webclient/src/store/rooms/rooms.reducer.spec.ts
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import { GameSortField, SortDirection } from 'types';
|
||||
import { roomsReducer } from './rooms.reducer';
|
||||
import { Types, MAX_ROOM_MESSAGES } from './rooms.types';
|
||||
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
||||
|
||||
// ── Initialisation ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('Initialisation', () => {
|
||||
it('returns initialState when called with undefined state', () => {
|
||||
const result = roomsReducer(undefined, { type: '@@INIT' });
|
||||
expect(result.rooms).toEqual({});
|
||||
expect(result.joinedRoomIds).toEqual({});
|
||||
});
|
||||
|
||||
it('CLEAR_STORE → resets to initialState', () => {
|
||||
const state = makeRoomsState({ joinedRoomIds: { 1: true } });
|
||||
const result = roomsReducer(state, { type: Types.CLEAR_STORE });
|
||||
expect(result.joinedRoomIds).toEqual({});
|
||||
expect(result.rooms).toEqual({});
|
||||
});
|
||||
|
||||
it('default → returns state unchanged for unknown action', () => {
|
||||
const state = makeRoomsState();
|
||||
const result = roomsReducer(state, { type: '@@UNKNOWN' });
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UPDATE_ROOMS ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('UPDATE_ROOMS', () => {
|
||||
it('merges rooms and strips gameList, gametypeList, userList from update', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const room = { ...makeRoom({ roomId: 1 }), gameList: [makeGame()], userList: [makeUser()], gametypeList: ['standard'] };
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [room] });
|
||||
expect(result.rooms[1]).toBeDefined();
|
||||
expect(result.rooms[1].gameList).toBeUndefined();
|
||||
expect(result.rooms[1].userList).toBeUndefined();
|
||||
expect(result.rooms[1].gametypeList).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sets numeric order from array index', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const rooms = [makeRoom({ roomId: 1 }), makeRoom({ roomId: 2 })];
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms });
|
||||
expect(result.rooms[1].order).toBe(0);
|
||||
expect(result.rooms[2].order).toBe(1);
|
||||
});
|
||||
|
||||
it('merges into existing room entry (preserves existing fields)', () => {
|
||||
const existingRoom = makeRoom({ roomId: 1, name: 'Old Name', gameList: [makeGame()] });
|
||||
const state = makeRoomsState({ rooms: { 1: existingRoom } });
|
||||
const update = makeRoom({ roomId: 1, name: 'New Name' });
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [update] });
|
||||
expect(result.rooms[1].name).toBe('New Name');
|
||||
expect(result.rooms[1].gameList).toEqual([makeGame()]);
|
||||
});
|
||||
|
||||
it('creates new room entry for unknown roomId', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const room = makeRoom({ roomId: 99, name: 'New Room' });
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_ROOMS, rooms: [room] });
|
||||
expect(result.rooms[99]).toBeDefined();
|
||||
expect(result.rooms[99].name).toBe('New Room');
|
||||
});
|
||||
});
|
||||
|
||||
// ── JOIN_ROOM ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('JOIN_ROOM', () => {
|
||||
it('copies gameList and userList, sorts both, sets joinedRoomIds', () => {
|
||||
const state = makeRoomsState({ rooms: {}, joinedRoomIds: {} });
|
||||
const roomInfo = makeRoom({
|
||||
roomId: 2,
|
||||
gameList: [makeGame({ gameId: 1 })],
|
||||
userList: [makeUser({ name: 'Zane' }), makeUser({ name: 'Alice' })],
|
||||
});
|
||||
const result = roomsReducer(state, { type: Types.JOIN_ROOM, roomInfo });
|
||||
expect(result.joinedRoomIds[2]).toBe(true);
|
||||
expect(result.rooms[2].userList[0].name).toBe('Alice');
|
||||
expect(result.rooms[2]).toMatchObject({ roomId: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
// ── LEAVE_ROOM ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('LEAVE_ROOM', () => {
|
||||
it('removes joinedRoomIds entry and messages for roomId', () => {
|
||||
const state = makeRoomsState({
|
||||
joinedRoomIds: { 1: true },
|
||||
messages: { 1: [makeMessage()] },
|
||||
});
|
||||
const result = roomsReducer(state, { type: Types.LEAVE_ROOM, roomId: 1 });
|
||||
expect(result.joinedRoomIds[1]).toBeUndefined();
|
||||
expect(result.messages[1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── ADD_MESSAGE ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ADD_MESSAGE', () => {
|
||||
it('appends message with timeReceived set', () => {
|
||||
const state = makeRoomsState({ messages: { 1: [] } });
|
||||
const message = makeMessage({ message: 'hello', timeReceived: 0 });
|
||||
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
|
||||
expect(result.messages[1]).toHaveLength(1);
|
||||
expect(result.messages[1][0].timeReceived).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('creates message list for roomId when none exists', () => {
|
||||
const state = makeRoomsState({ messages: {} });
|
||||
const message = makeMessage();
|
||||
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 5, message });
|
||||
expect(result.messages[5]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it(`shifts oldest message when list is at MAX_ROOM_MESSAGES (${MAX_ROOM_MESSAGES})`, () => {
|
||||
const firstMsg = makeMessage({ message: 'first' });
|
||||
const messages = Array.from({ length: MAX_ROOM_MESSAGES }, (_, i) =>
|
||||
i === 0 ? firstMsg : makeMessage({ message: `msg-${i}` })
|
||||
);
|
||||
const state = makeRoomsState({ messages: { 1: messages } });
|
||||
const newMsg = makeMessage({ message: 'new' });
|
||||
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message: newMsg });
|
||||
expect(result.messages[1]).toHaveLength(MAX_ROOM_MESSAGES);
|
||||
expect(result.messages[1][0].message).not.toBe('first');
|
||||
expect(result.messages[1][MAX_ROOM_MESSAGES - 1].message).toBe('new');
|
||||
});
|
||||
});
|
||||
|
||||
// ── UPDATE_GAMES ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('UPDATE_GAMES', () => {
|
||||
it('removes closed games from gameList', () => {
|
||||
const room = makeRoom({ roomId: 1, gameList: [makeGame({ gameId: 1 })] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
roomId: 1,
|
||||
games: [{ gameId: 1, closed: true }],
|
||||
});
|
||||
expect(result.rooms[1].gameList).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('merges update into existing game', () => {
|
||||
const game = makeGame({ gameId: 1, description: 'old' });
|
||||
const room = makeRoom({ roomId: 1, gameList: [game] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
roomId: 1,
|
||||
games: [{ gameId: 1, description: 'new' }],
|
||||
});
|
||||
expect(result.rooms[1].gameList[0].description).toBe('new');
|
||||
});
|
||||
|
||||
it('appends new game to list and sorts', () => {
|
||||
const room = makeRoom({ roomId: 1, gameList: [] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const newGame = makeGame({ gameId: 99, description: 'extra' });
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 1, games: [newGame] });
|
||||
expect(result.rooms[1].gameList).toHaveLength(1);
|
||||
expect(result.rooms[1].gameList[0].gameId).toBe(99);
|
||||
});
|
||||
|
||||
it('preserves existing games not included in the update', () => {
|
||||
const game1 = makeGame({ gameId: 1, description: 'untouched' });
|
||||
const game2 = makeGame({ gameId: 2, description: 'old' });
|
||||
const room = makeRoom({ roomId: 1, gameList: [game1, game2] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.UPDATE_GAMES,
|
||||
roomId: 1,
|
||||
games: [{ gameId: 2, description: 'new' }],
|
||||
});
|
||||
expect(result.rooms[1].gameList.find(g => g.gameId === 1).description).toBe('untouched');
|
||||
expect(result.rooms[1].gameList.find(g => g.gameId === 2).description).toBe('new');
|
||||
});
|
||||
|
||||
it('returns { ...state } (not identity) when roomId is unknown', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 999, games: [] });
|
||||
expect(result).not.toBe(state);
|
||||
expect(result.rooms).toEqual(state.rooms);
|
||||
});
|
||||
});
|
||||
|
||||
// ── USER_JOINED / USER_LEFT ───────────────────────────────────────────────────
|
||||
|
||||
describe('USER_JOINED', () => {
|
||||
it('appends user to userList and sorts by name ASC', () => {
|
||||
const room = makeRoom({ roomId: 1, userList: [makeUser({ name: 'Zane' })] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, { type: Types.USER_JOINED, roomId: 1, user: makeUser({ name: 'Alice' }) });
|
||||
expect(result.rooms[1].userList[0].name).toBe('Alice');
|
||||
expect(result.rooms[1].userList).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('USER_LEFT', () => {
|
||||
it('removes user by name from userList', () => {
|
||||
const room = makeRoom({ roomId: 1, userList: [makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })] });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, { type: Types.USER_LEFT, roomId: 1, name: 'Alice' });
|
||||
expect(result.rooms[1].userList).toHaveLength(1);
|
||||
expect(result.rooms[1].userList[0].name).toBe('Bob');
|
||||
});
|
||||
});
|
||||
|
||||
// ── SORT_GAMES ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('SORT_GAMES', () => {
|
||||
it('resorts gameList and updates sortGamesBy on state', () => {
|
||||
const games = [makeGame({ gameId: 2 }), makeGame({ gameId: 1 })];
|
||||
const room = makeRoom({ roomId: 1, gameList: games });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const result = roomsReducer(state, {
|
||||
type: Types.SORT_GAMES,
|
||||
roomId: 1,
|
||||
field: GameSortField.START_TIME,
|
||||
order: SortDirection.ASC,
|
||||
});
|
||||
expect(result.sortGamesBy).toEqual({ field: GameSortField.START_TIME, order: SortDirection.ASC });
|
||||
});
|
||||
});
|
||||
|
||||
// ── REMOVE_MESSAGES ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('REMOVE_MESSAGES', () => {
|
||||
it('removes messages starting with "name:" up to amount, in reverse scan order', () => {
|
||||
const msgs = [
|
||||
makeMessage({ message: 'Alice: hello' }),
|
||||
makeMessage({ message: 'Bob: hi' }),
|
||||
makeMessage({ message: 'Alice: world' }),
|
||||
];
|
||||
const state = makeRoomsState({ messages: { 1: msgs } });
|
||||
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
|
||||
// reverse scan: removes LAST 'Alice:' message first, stops after 1
|
||||
const remaining = result.messages[1];
|
||||
expect(remaining).toHaveLength(2);
|
||||
const aliceMessages = remaining.filter(m => m.message.startsWith('Alice:'));
|
||||
expect(aliceMessages).toHaveLength(1);
|
||||
expect(aliceMessages[0].message).toBe('Alice: hello');
|
||||
});
|
||||
|
||||
it('removes up to amount matching messages', () => {
|
||||
const msgs = [
|
||||
makeMessage({ message: 'Alice: one' }),
|
||||
makeMessage({ message: 'Alice: two' }),
|
||||
makeMessage({ message: 'Alice: three' }),
|
||||
];
|
||||
const state = makeRoomsState({ messages: { 1: msgs } });
|
||||
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 2 });
|
||||
const remaining = result.messages[1];
|
||||
expect(remaining).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('stops removing once amount is reached', () => {
|
||||
const msgs = [
|
||||
makeMessage({ message: 'Alice: a' }),
|
||||
makeMessage({ message: 'Alice: b' }),
|
||||
makeMessage({ message: 'Alice: c' }),
|
||||
];
|
||||
const state = makeRoomsState({ messages: { 1: msgs } });
|
||||
const result = roomsReducer(state, { type: Types.REMOVE_MESSAGES, roomId: 1, name: 'Alice', amount: 1 });
|
||||
expect(result.messages[1]).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── JOINED_GAME ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('JOINED_GAME', () => {
|
||||
it('sets joinedGameIds[roomId][gameId] = true', () => {
|
||||
const state = makeRoomsState({ joinedGameIds: {} });
|
||||
const result = roomsReducer(state, { type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
|
||||
expect(result.joinedGameIds[1][5]).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves other roomId entries in joinedGameIds', () => {
|
||||
const state = makeRoomsState({ joinedGameIds: { 2: { 9: true } } });
|
||||
const result = roomsReducer(state, { type: Types.JOINED_GAME, roomId: 1, gameId: 5 });
|
||||
expect(result.joinedGameIds[2][9]).toBe(true);
|
||||
expect(result.joinedGameIds[1][5]).toBe(true);
|
||||
});
|
||||
});
|
||||
107
webclient/src/store/rooms/rooms.selectors.spec.ts
Normal file
107
webclient/src/store/rooms/rooms.selectors.spec.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Selectors } from './rooms.selectors';
|
||||
import { RoomsState } from './rooms.interfaces';
|
||||
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
||||
|
||||
function rootState(rooms: RoomsState) {
|
||||
return { rooms };
|
||||
}
|
||||
|
||||
describe('Selectors', () => {
|
||||
it('getRooms → returns rooms map', () => {
|
||||
const state = makeRoomsState();
|
||||
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 } });
|
||||
expect(Selectors.getRoom(rootState(state), 1)).toBe(room);
|
||||
});
|
||||
|
||||
it('getRoom → returns undefined for unknown roomId', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
expect(Selectors.getRoom(rootState(state), 999)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getJoinedRoomIds → returns joinedRoomIds', () => {
|
||||
const joinedRoomIds = { 1: true };
|
||||
const state = makeRoomsState({ joinedRoomIds });
|
||||
expect(Selectors.getJoinedRoomIds(rootState(state))).toBe(joinedRoomIds);
|
||||
});
|
||||
|
||||
it('getJoinedGameIds → returns joinedGameIds', () => {
|
||||
const joinedGameIds = { 1: { 5: true } };
|
||||
const state = makeRoomsState({ joinedGameIds });
|
||||
expect(Selectors.getJoinedGameIds(rootState(state))).toBe(joinedGameIds);
|
||||
});
|
||||
|
||||
it('getMessages → returns messages map', () => {
|
||||
const messages = { 1: [makeMessage()] };
|
||||
const state = makeRoomsState({ messages });
|
||||
expect(Selectors.getMessages(rootState(state))).toBe(messages);
|
||||
});
|
||||
|
||||
it('getSortGamesBy → returns sortGamesBy', () => {
|
||||
const state = makeRoomsState();
|
||||
expect(Selectors.getSortGamesBy(rootState(state))).toBe(state.sortGamesBy);
|
||||
});
|
||||
|
||||
it('getSortUsersBy → returns sortUsersBy', () => {
|
||||
const state = makeRoomsState();
|
||||
expect(Selectors.getSortUsersBy(rootState(state))).toBe(state.sortUsersBy);
|
||||
});
|
||||
|
||||
it('getJoinedRooms → returns only rooms whose roomId is in joinedRoomIds', () => {
|
||||
const room1 = makeRoom({ roomId: 1 });
|
||||
const room2 = makeRoom({ roomId: 2 });
|
||||
const state = makeRoomsState({
|
||||
rooms: { 1: room1, 2: room2 },
|
||||
joinedRoomIds: { 1: true },
|
||||
});
|
||||
const result = Selectors.getJoinedRooms(rootState(state));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(room1);
|
||||
});
|
||||
|
||||
it('getJoinedRooms → returns empty array when none joined', () => {
|
||||
const state = makeRoomsState({ rooms: { 1: makeRoom({ roomId: 1 }) }, joinedRoomIds: {} });
|
||||
expect(Selectors.getJoinedRooms(rootState(state))).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('getJoinedGames → returns only games whose gameId is in joinedGameIds for that room', () => {
|
||||
const game1 = makeGame({ gameId: 1 });
|
||||
const game2 = makeGame({ gameId: 2 });
|
||||
const state = makeRoomsState({
|
||||
games: { 1: { 1: game1, 2: game2 } },
|
||||
joinedGameIds: { 1: { 1: true } },
|
||||
});
|
||||
const result = Selectors.getJoinedGames(rootState(state), 1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(game1);
|
||||
});
|
||||
|
||||
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 games = [makeGame()];
|
||||
const room = makeRoom({ roomId: 1, gameList: games });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
expect(Selectors.getRoomGames(rootState(state), 1)).toBe(games);
|
||||
});
|
||||
|
||||
it('getRoomUsers → returns userList for roomId', () => {
|
||||
const users = [makeUser()];
|
||||
const room = makeRoom({ roomId: 1, userList: users });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(users);
|
||||
});
|
||||
});
|
||||
154
webclient/src/store/server/__mocks__/server-fixtures.ts
Normal file
154
webclient/src/store/server/__mocks__/server-fixtures.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import {
|
||||
BanHistoryItem,
|
||||
DeckList,
|
||||
DeckStorageTreeItem,
|
||||
LogItem,
|
||||
ReplayMatch,
|
||||
SortDirection,
|
||||
StatusEnum,
|
||||
User,
|
||||
UserPrivLevel,
|
||||
UserSortField,
|
||||
WebSocketConnectOptions,
|
||||
WarnHistoryItem,
|
||||
WarnListItem,
|
||||
} from 'types';
|
||||
import { ServerState } from '../server.interfaces';
|
||||
|
||||
export function makeUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
name: 'TestUser',
|
||||
accountageSecs: 0,
|
||||
privlevel: UserPrivLevel.NONE,
|
||||
userLevel: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeLogItem(overrides: Partial<LogItem> = {}): LogItem {
|
||||
return {
|
||||
message: '',
|
||||
senderId: '',
|
||||
senderIp: '',
|
||||
senderName: '',
|
||||
targetId: '',
|
||||
targetName: '',
|
||||
targetType: '',
|
||||
time: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeBanHistoryItem(overrides: Partial<BanHistoryItem> = {}): BanHistoryItem {
|
||||
return {
|
||||
adminId: '',
|
||||
adminName: '',
|
||||
banTime: '',
|
||||
banLength: '',
|
||||
banReason: '',
|
||||
visibleReason: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeWarnHistoryItem(overrides: Partial<WarnHistoryItem> = {}): WarnHistoryItem {
|
||||
return {
|
||||
userName: '',
|
||||
adminName: '',
|
||||
reason: '',
|
||||
timeOf: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeWarnListItem(overrides: Partial<WarnListItem> = {}): WarnListItem {
|
||||
return {
|
||||
warning: '',
|
||||
userName: '',
|
||||
userClientid: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeDeckTreeItem(overrides: Partial<DeckStorageTreeItem> = {}): DeckStorageTreeItem {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'item',
|
||||
file: { creationTime: 0 },
|
||||
folder: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeDeckList(overrides: Partial<DeckList> = {}): DeckList {
|
||||
return {
|
||||
root: { items: [] },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeReplayMatch(overrides: Partial<ReplayMatch> = {}): ReplayMatch {
|
||||
return {
|
||||
gameId: 1,
|
||||
roomName: 'Test Room',
|
||||
timeStarted: 0,
|
||||
length: 0,
|
||||
gameName: 'Test Game',
|
||||
playerNames: [],
|
||||
doNotHide: false,
|
||||
replayList: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeConnectOptions(overrides: Partial<WebSocketConnectOptions> = {}): WebSocketConnectOptions {
|
||||
return {
|
||||
host: 'localhost',
|
||||
port: '4747',
|
||||
userName: 'user',
|
||||
password: 'pass',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeServerState(overrides: Partial<ServerState> = {}): ServerState {
|
||||
return {
|
||||
initialized: false,
|
||||
buddyList: [],
|
||||
ignoreList: [],
|
||||
status: {
|
||||
state: StatusEnum.DISCONNECTED,
|
||||
description: null,
|
||||
},
|
||||
info: {
|
||||
message: null,
|
||||
name: null,
|
||||
version: null,
|
||||
},
|
||||
logs: {
|
||||
room: [],
|
||||
game: [],
|
||||
chat: [],
|
||||
},
|
||||
user: null,
|
||||
users: [],
|
||||
sortUsersBy: {
|
||||
field: UserSortField.NAME,
|
||||
order: SortDirection.ASC,
|
||||
},
|
||||
connectOptions: {},
|
||||
messages: {},
|
||||
userInfo: {},
|
||||
notifications: [],
|
||||
serverShutdown: null,
|
||||
banUser: '',
|
||||
banHistory: {},
|
||||
warnHistory: {},
|
||||
warnListOptions: [],
|
||||
warnUser: '',
|
||||
adminNotes: {},
|
||||
replays: [],
|
||||
backendDecks: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
356
webclient/src/store/server/server.actions.spec.ts
Normal file
356
webclient/src/store/server/server.actions.spec.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import { Actions } from './server.actions';
|
||||
import { Types } from './server.types';
|
||||
import {
|
||||
makeBanHistoryItem,
|
||||
makeConnectOptions,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeReplayMatch,
|
||||
makeUser,
|
||||
makeWarnHistoryItem,
|
||||
makeWarnListItem,
|
||||
} from './__mocks__/server-fixtures';
|
||||
|
||||
describe('Actions', () => {
|
||||
it('initialized', () => {
|
||||
expect(Actions.initialized()).toEqual({ type: Types.INITIALIZED });
|
||||
});
|
||||
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
});
|
||||
|
||||
it('loginSuccessful', () => {
|
||||
const options = makeConnectOptions();
|
||||
expect(Actions.loginSuccessful(options)).toEqual({ type: Types.LOGIN_SUCCESSFUL, options });
|
||||
});
|
||||
|
||||
it('loginFailed', () => {
|
||||
expect(Actions.loginFailed()).toEqual({ type: Types.LOGIN_FAILED });
|
||||
});
|
||||
|
||||
it('connectionClosed', () => {
|
||||
expect(Actions.connectionClosed(3)).toEqual({ type: Types.CONNECTION_CLOSED, reason: 3 });
|
||||
});
|
||||
|
||||
it('connectionFailed', () => {
|
||||
expect(Actions.connectionFailed()).toEqual({ type: Types.CONNECTION_FAILED });
|
||||
});
|
||||
|
||||
it('testConnectionSuccessful', () => {
|
||||
expect(Actions.testConnectionSuccessful()).toEqual({ type: Types.TEST_CONNECTION_SUCCESSFUL });
|
||||
});
|
||||
|
||||
it('testConnectionFailed', () => {
|
||||
expect(Actions.testConnectionFailed()).toEqual({ type: Types.TEST_CONNECTION_FAILED });
|
||||
});
|
||||
|
||||
it('serverMessage', () => {
|
||||
expect(Actions.serverMessage('hello')).toEqual({ type: Types.SERVER_MESSAGE, message: 'hello' });
|
||||
});
|
||||
|
||||
it('updateBuddyList', () => {
|
||||
const list = [makeUser()];
|
||||
expect(Actions.updateBuddyList(list)).toEqual({ type: Types.UPDATE_BUDDY_LIST, buddyList: list });
|
||||
});
|
||||
|
||||
it('addToBuddyList', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.addToBuddyList(user)).toEqual({ type: Types.ADD_TO_BUDDY_LIST, user });
|
||||
});
|
||||
|
||||
it('removeFromBuddyList', () => {
|
||||
expect(Actions.removeFromBuddyList('Alice')).toEqual({ type: Types.REMOVE_FROM_BUDDY_LIST, userName: 'Alice' });
|
||||
});
|
||||
|
||||
it('updateIgnoreList', () => {
|
||||
const list = [makeUser()];
|
||||
expect(Actions.updateIgnoreList(list)).toEqual({ type: Types.UPDATE_IGNORE_LIST, ignoreList: list });
|
||||
});
|
||||
|
||||
it('addToIgnoreList', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.addToIgnoreList(user)).toEqual({ type: Types.ADD_TO_IGNORE_LIST, user });
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList', () => {
|
||||
expect(Actions.removeFromIgnoreList('Bob')).toEqual({ type: Types.REMOVE_FROM_IGNORE_LIST, userName: 'Bob' });
|
||||
});
|
||||
|
||||
it('updateInfo', () => {
|
||||
const info = { name: 'Servatrice', version: '2.0' };
|
||||
expect(Actions.updateInfo(info)).toEqual({ type: Types.UPDATE_INFO, info });
|
||||
});
|
||||
|
||||
it('updateStatus', () => {
|
||||
const status = { state: 2, description: 'connected' };
|
||||
expect(Actions.updateStatus(status)).toEqual({ type: Types.UPDATE_STATUS, status });
|
||||
});
|
||||
|
||||
it('updateUser', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.updateUser(user)).toEqual({ type: Types.UPDATE_USER, user });
|
||||
});
|
||||
|
||||
it('updateUsers', () => {
|
||||
const users = [makeUser()];
|
||||
expect(Actions.updateUsers(users)).toEqual({ type: Types.UPDATE_USERS, users });
|
||||
});
|
||||
|
||||
it('userJoined', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.userJoined(user)).toEqual({ type: Types.USER_JOINED, user });
|
||||
});
|
||||
|
||||
it('userLeft', () => {
|
||||
expect(Actions.userLeft('Carol')).toEqual({ type: Types.USER_LEFT, name: 'Carol' });
|
||||
});
|
||||
|
||||
it('viewLogs', () => {
|
||||
const logs = { room: [], game: [], chat: [] };
|
||||
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
|
||||
});
|
||||
|
||||
it('clearLogs', () => {
|
||||
expect(Actions.clearLogs()).toEqual({ type: Types.CLEAR_LOGS });
|
||||
});
|
||||
|
||||
it('registrationRequiresEmail', () => {
|
||||
expect(Actions.registrationRequiresEmail()).toEqual({ type: Types.REGISTRATION_REQUIRES_EMAIL });
|
||||
});
|
||||
|
||||
it('registrationSuccess', () => {
|
||||
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCES });
|
||||
});
|
||||
|
||||
it('registrationFailed', () => {
|
||||
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, error: 'err' });
|
||||
});
|
||||
|
||||
it('registrationEmailError', () => {
|
||||
expect(Actions.registrationEmailError('bad email')).toEqual({ type: Types.REGISTRATION_EMAIL_ERROR, error: 'bad email' });
|
||||
});
|
||||
|
||||
it('registrationPasswordError', () => {
|
||||
expect(Actions.registrationPasswordError('bad pw')).toEqual({ type: Types.REGISTRATION_PASSWORD_ERROR, error: 'bad pw' });
|
||||
});
|
||||
|
||||
it('registrationUserNameError', () => {
|
||||
expect(Actions.registrationUserNameError('bad name')).toEqual({ type: Types.REGISTRATION_USERNAME_ERROR, error: 'bad name' });
|
||||
});
|
||||
|
||||
it('accountAwaitingActivation', () => {
|
||||
const options = makeConnectOptions();
|
||||
expect(Actions.accountAwaitingActivation(options)).toEqual({ type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
|
||||
});
|
||||
|
||||
it('accountActivationSuccess', () => {
|
||||
expect(Actions.accountActivationSuccess()).toEqual({ type: Types.ACCOUNT_ACTIVATION_SUCCESS });
|
||||
});
|
||||
|
||||
it('accountActivationFailed', () => {
|
||||
expect(Actions.accountActivationFailed()).toEqual({ type: Types.ACCOUNT_ACTIVATION_FAILED });
|
||||
});
|
||||
|
||||
it('resetPassword', () => {
|
||||
expect(Actions.resetPassword()).toEqual({ type: Types.RESET_PASSWORD_REQUESTED });
|
||||
});
|
||||
|
||||
it('resetPasswordFailed', () => {
|
||||
expect(Actions.resetPasswordFailed()).toEqual({ type: Types.RESET_PASSWORD_FAILED });
|
||||
});
|
||||
|
||||
it('resetPasswordChallenge', () => {
|
||||
expect(Actions.resetPasswordChallenge()).toEqual({ type: Types.RESET_PASSWORD_CHALLENGE });
|
||||
});
|
||||
|
||||
it('resetPasswordSuccess', () => {
|
||||
expect(Actions.resetPasswordSuccess()).toEqual({ type: Types.RESET_PASSWORD_SUCCESS });
|
||||
});
|
||||
|
||||
it('adjustMod', () => {
|
||||
expect(Actions.adjustMod('Dan', true, false)).toEqual({
|
||||
type: Types.ADJUST_MOD,
|
||||
userName: 'Dan',
|
||||
shouldBeMod: true,
|
||||
shouldBeJudge: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('reloadConfig', () => {
|
||||
expect(Actions.reloadConfig()).toEqual({ type: Types.RELOAD_CONFIG });
|
||||
});
|
||||
|
||||
it('shutdownServer', () => {
|
||||
expect(Actions.shutdownServer()).toEqual({ type: Types.SHUTDOWN_SERVER });
|
||||
});
|
||||
|
||||
it('updateServerMessage', () => {
|
||||
expect(Actions.updateServerMessage()).toEqual({ type: Types.UPDATE_SERVER_MESSAGE });
|
||||
});
|
||||
|
||||
it('accountPasswordChange', () => {
|
||||
expect(Actions.accountPasswordChange()).toEqual({ type: Types.ACCOUNT_PASSWORD_CHANGE });
|
||||
});
|
||||
|
||||
it('accountEditChanged', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.accountEditChanged(user)).toEqual({ type: Types.ACCOUNT_EDIT_CHANGED, user });
|
||||
});
|
||||
|
||||
it('accountImageChanged', () => {
|
||||
const user = makeUser();
|
||||
expect(Actions.accountImageChanged(user)).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, user });
|
||||
});
|
||||
|
||||
it('directMessageSent', () => {
|
||||
expect(Actions.directMessageSent('Eve', 'hi')).toEqual({
|
||||
type: Types.DIRECT_MESSAGE_SENT,
|
||||
userName: 'Eve',
|
||||
message: 'hi',
|
||||
});
|
||||
});
|
||||
|
||||
it('getUserInfo', () => {
|
||||
const userInfo = makeUser({ name: 'Frank' });
|
||||
expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo });
|
||||
});
|
||||
|
||||
it('notifyUser', () => {
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
||||
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
|
||||
});
|
||||
|
||||
it('serverShutdown', () => {
|
||||
const data = { reason: 'maintenance', minutes: 5 };
|
||||
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
|
||||
});
|
||||
|
||||
it('userMessage', () => {
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
||||
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
|
||||
});
|
||||
|
||||
it('addToList', () => {
|
||||
expect(Actions.addToList('buddyList', 'Grace')).toEqual({
|
||||
type: Types.ADD_TO_LIST,
|
||||
list: 'buddyList',
|
||||
userName: 'Grace',
|
||||
});
|
||||
});
|
||||
|
||||
it('removeFromList', () => {
|
||||
expect(Actions.removeFromList('buddyList', 'Hank')).toEqual({
|
||||
type: Types.REMOVE_FROM_LIST,
|
||||
list: 'buddyList',
|
||||
userName: 'Hank',
|
||||
});
|
||||
});
|
||||
|
||||
it('banFromServer', () => {
|
||||
expect(Actions.banFromServer('Ira')).toEqual({ type: Types.BAN_FROM_SERVER, userName: 'Ira' });
|
||||
});
|
||||
|
||||
it('banHistory', () => {
|
||||
const history = [makeBanHistoryItem()];
|
||||
expect(Actions.banHistory('Ira', history)).toEqual({ type: Types.BAN_HISTORY, userName: 'Ira', banHistory: history });
|
||||
});
|
||||
|
||||
it('warnHistory', () => {
|
||||
const history = [makeWarnHistoryItem()];
|
||||
expect(Actions.warnHistory('Jack', history)).toEqual({ type: Types.WARN_HISTORY, userName: 'Jack', warnHistory: history });
|
||||
});
|
||||
|
||||
it('warnListOptions', () => {
|
||||
const list = [makeWarnListItem()];
|
||||
expect(Actions.warnListOptions(list)).toEqual({ type: Types.WARN_LIST_OPTIONS, warnList: list });
|
||||
});
|
||||
|
||||
it('warnUser', () => {
|
||||
expect(Actions.warnUser('Kelly')).toEqual({ type: Types.WARN_USER, userName: 'Kelly' });
|
||||
});
|
||||
|
||||
it('grantReplayAccess', () => {
|
||||
expect(Actions.grantReplayAccess(7, 'Moe')).toEqual({
|
||||
type: Types.GRANT_REPLAY_ACCESS,
|
||||
replayId: 7,
|
||||
moderatorName: 'Moe',
|
||||
});
|
||||
});
|
||||
|
||||
it('forceActivateUser', () => {
|
||||
expect(Actions.forceActivateUser('Ned', 'Moe')).toEqual({
|
||||
type: Types.FORCE_ACTIVATE_USER,
|
||||
usernameToActivate: 'Ned',
|
||||
moderatorName: 'Moe',
|
||||
});
|
||||
});
|
||||
|
||||
it('getAdminNotes', () => {
|
||||
expect(Actions.getAdminNotes('Ned', 'some notes')).toEqual({
|
||||
type: Types.GET_ADMIN_NOTES,
|
||||
userName: 'Ned',
|
||||
notes: 'some notes',
|
||||
});
|
||||
});
|
||||
|
||||
it('updateAdminNotes', () => {
|
||||
expect(Actions.updateAdminNotes('Ned', 'updated notes')).toEqual({
|
||||
type: Types.UPDATE_ADMIN_NOTES,
|
||||
userName: 'Ned',
|
||||
notes: 'updated notes',
|
||||
});
|
||||
});
|
||||
|
||||
it('replayList', () => {
|
||||
const list = [makeReplayMatch()];
|
||||
expect(Actions.replayList(list)).toEqual({ type: Types.REPLAY_LIST, matchList: list });
|
||||
});
|
||||
|
||||
it('replayAdded', () => {
|
||||
const match = makeReplayMatch();
|
||||
expect(Actions.replayAdded(match)).toEqual({ type: Types.REPLAY_ADDED, matchInfo: match });
|
||||
});
|
||||
|
||||
it('replayModifyMatch', () => {
|
||||
expect(Actions.replayModifyMatch(5, true)).toEqual({
|
||||
type: Types.REPLAY_MODIFY_MATCH,
|
||||
gameId: 5,
|
||||
doNotHide: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('replayDeleteMatch', () => {
|
||||
expect(Actions.replayDeleteMatch(5)).toEqual({ type: Types.REPLAY_DELETE_MATCH, gameId: 5 });
|
||||
});
|
||||
|
||||
it('backendDecks', () => {
|
||||
const deckList = makeDeckList();
|
||||
expect(Actions.backendDecks(deckList)).toEqual({ type: Types.BACKEND_DECKS, deckList });
|
||||
});
|
||||
|
||||
it('deckNewDir', () => {
|
||||
expect(Actions.deckNewDir('a/b', 'newFolder')).toEqual({
|
||||
type: Types.DECK_NEW_DIR,
|
||||
path: 'a/b',
|
||||
dirName: 'newFolder',
|
||||
});
|
||||
});
|
||||
|
||||
it('deckDelDir', () => {
|
||||
expect(Actions.deckDelDir('a/b')).toEqual({ type: Types.DECK_DEL_DIR, path: 'a/b' });
|
||||
});
|
||||
|
||||
it('deckUpload', () => {
|
||||
const treeItem = makeDeckTreeItem();
|
||||
expect(Actions.deckUpload('a/b', treeItem)).toEqual({
|
||||
type: Types.DECK_UPLOAD,
|
||||
path: 'a/b',
|
||||
treeItem,
|
||||
});
|
||||
});
|
||||
|
||||
it('deckDelete', () => {
|
||||
expect(Actions.deckDelete(42)).toEqual({ type: Types.DECK_DELETE, deckId: 42 });
|
||||
});
|
||||
});
|
||||
388
webclient/src/store/server/server.dispatch.spec.ts
Normal file
388
webclient/src/store/server/server.dispatch.spec.ts
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
jest.mock('store/store', () => ({ store: { dispatch: jest.fn() } }));
|
||||
jest.mock('redux-form', () => ({
|
||||
reset: jest.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
|
||||
}));
|
||||
|
||||
import { store } from 'store/store';
|
||||
import { reset } from 'redux-form';
|
||||
import { Actions } from './server.actions';
|
||||
import { Dispatch } from './server.dispatch';
|
||||
import {
|
||||
makeBanHistoryItem,
|
||||
makeConnectOptions,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeReplayMatch,
|
||||
makeUser,
|
||||
makeWarnHistoryItem,
|
||||
makeWarnListItem,
|
||||
} from './__mocks__/server-fixtures';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('Dispatch', () => {
|
||||
it('initialized dispatches Actions.initialized()', () => {
|
||||
Dispatch.initialized();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.initialized());
|
||||
});
|
||||
|
||||
it('clearStore dispatches Actions.clearStore()', () => {
|
||||
Dispatch.clearStore();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.clearStore());
|
||||
});
|
||||
|
||||
it('loginSuccessful dispatches Actions.loginSuccessful()', () => {
|
||||
const options = makeConnectOptions();
|
||||
Dispatch.loginSuccessful(options);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.loginSuccessful(options));
|
||||
});
|
||||
|
||||
it('loginFailed dispatches Actions.loginFailed()', () => {
|
||||
Dispatch.loginFailed();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.loginFailed());
|
||||
});
|
||||
|
||||
it('connectionClosed dispatches Actions.connectionClosed()', () => {
|
||||
Dispatch.connectionClosed(3);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionClosed(3));
|
||||
});
|
||||
|
||||
it('connectionFailed dispatches Actions.connectionFailed()', () => {
|
||||
Dispatch.connectionFailed();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionFailed());
|
||||
});
|
||||
|
||||
it('testConnectionSuccessful dispatches Actions.testConnectionSuccessful()', () => {
|
||||
Dispatch.testConnectionSuccessful();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.testConnectionSuccessful());
|
||||
});
|
||||
|
||||
it('testConnectionFailed dispatches Actions.testConnectionFailed()', () => {
|
||||
Dispatch.testConnectionFailed();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.testConnectionFailed());
|
||||
});
|
||||
|
||||
it('updateBuddyList dispatches Actions.updateBuddyList()', () => {
|
||||
const list = [makeUser()];
|
||||
Dispatch.updateBuddyList(list);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateBuddyList(list));
|
||||
});
|
||||
|
||||
it('addToBuddyList dispatches reset("addToBuddies") then Actions.addToBuddyList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToBuddyList(user);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('addToBuddies'));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToBuddyList(user));
|
||||
});
|
||||
|
||||
it('removeFromBuddyList dispatches Actions.removeFromBuddyList()', () => {
|
||||
Dispatch.removeFromBuddyList('Alice');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.removeFromBuddyList('Alice'));
|
||||
});
|
||||
|
||||
it('updateIgnoreList dispatches Actions.updateIgnoreList()', () => {
|
||||
const list = [makeUser()];
|
||||
Dispatch.updateIgnoreList(list);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateIgnoreList(list));
|
||||
});
|
||||
|
||||
it('addToIgnoreList dispatches reset("addToIgnore") then Actions.addToIgnoreList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToIgnoreList(user);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as jest.Mock)('addToIgnore'));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToIgnoreList(user));
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList dispatches Actions.removeFromIgnoreList()', () => {
|
||||
Dispatch.removeFromIgnoreList('Bob');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.removeFromIgnoreList('Bob'));
|
||||
});
|
||||
|
||||
it('updateInfo dispatches Actions.updateInfo({ name, version })', () => {
|
||||
Dispatch.updateInfo('Servatrice', '2.9');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateInfo({ name: 'Servatrice', version: '2.9' }));
|
||||
});
|
||||
|
||||
it('updateStatus dispatches Actions.updateStatus({ state, description })', () => {
|
||||
Dispatch.updateStatus(2, 'ok');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: 2, description: 'ok' }));
|
||||
});
|
||||
|
||||
it('updateUser dispatches Actions.updateUser()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.updateUser(user);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateUser(user));
|
||||
});
|
||||
|
||||
it('updateUsers dispatches Actions.updateUsers()', () => {
|
||||
const users = [makeUser()];
|
||||
Dispatch.updateUsers(users);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateUsers(users));
|
||||
});
|
||||
|
||||
it('userJoined dispatches Actions.userJoined()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.userJoined(user);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userJoined(user));
|
||||
});
|
||||
|
||||
it('userLeft dispatches Actions.userLeft()', () => {
|
||||
Dispatch.userLeft('Carol');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userLeft('Carol'));
|
||||
});
|
||||
|
||||
it('viewLogs dispatches Actions.viewLogs()', () => {
|
||||
const logs = { room: [], game: [], chat: [] };
|
||||
Dispatch.viewLogs(logs);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
|
||||
});
|
||||
|
||||
it('clearLogs dispatches Actions.clearLogs()', () => {
|
||||
Dispatch.clearLogs();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.clearLogs());
|
||||
});
|
||||
|
||||
it('serverMessage dispatches Actions.serverMessage()', () => {
|
||||
Dispatch.serverMessage('Welcome!');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverMessage('Welcome!'));
|
||||
});
|
||||
|
||||
it('registrationRequiresEmail dispatches correctly', () => {
|
||||
Dispatch.registrationRequiresEmail();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationRequiresEmail());
|
||||
});
|
||||
|
||||
it('registrationSuccess dispatches correctly', () => {
|
||||
Dispatch.registrationSuccess();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationSuccess());
|
||||
});
|
||||
|
||||
it('registrationFailed dispatches correctly', () => {
|
||||
Dispatch.registrationFailed('err');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('err'));
|
||||
});
|
||||
|
||||
it('registrationEmailError dispatches correctly', () => {
|
||||
Dispatch.registrationEmailError('bad');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationEmailError('bad'));
|
||||
});
|
||||
|
||||
it('registrationPasswordError dispatches correctly', () => {
|
||||
Dispatch.registrationPasswordError('weak');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationPasswordError('weak'));
|
||||
});
|
||||
|
||||
it('registrationUserNameError dispatches correctly', () => {
|
||||
Dispatch.registrationUserNameError('taken');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationUserNameError('taken'));
|
||||
});
|
||||
|
||||
it('accountAwaitingActivation dispatches correctly', () => {
|
||||
const options = makeConnectOptions();
|
||||
Dispatch.accountAwaitingActivation(options);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountAwaitingActivation(options));
|
||||
});
|
||||
|
||||
it('accountActivationSuccess dispatches correctly', () => {
|
||||
Dispatch.accountActivationSuccess();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountActivationSuccess());
|
||||
});
|
||||
|
||||
it('accountActivationFailed dispatches correctly', () => {
|
||||
Dispatch.accountActivationFailed();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountActivationFailed());
|
||||
});
|
||||
|
||||
it('resetPassword dispatches correctly', () => {
|
||||
Dispatch.resetPassword();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.resetPassword());
|
||||
});
|
||||
|
||||
it('resetPasswordFailed dispatches correctly', () => {
|
||||
Dispatch.resetPasswordFailed();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.resetPasswordFailed());
|
||||
});
|
||||
|
||||
it('resetPasswordChallenge dispatches correctly', () => {
|
||||
Dispatch.resetPasswordChallenge();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.resetPasswordChallenge());
|
||||
});
|
||||
|
||||
it('resetPasswordSuccess dispatches correctly', () => {
|
||||
Dispatch.resetPasswordSuccess();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.resetPasswordSuccess());
|
||||
});
|
||||
|
||||
it('adjustMod dispatches Actions.adjustMod()', () => {
|
||||
Dispatch.adjustMod('Dan', true, false);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.adjustMod('Dan', true, false));
|
||||
});
|
||||
|
||||
it('reloadConfig dispatches correctly', () => {
|
||||
Dispatch.reloadConfig();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.reloadConfig());
|
||||
});
|
||||
|
||||
it('shutdownServer dispatches correctly', () => {
|
||||
Dispatch.shutdownServer();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.shutdownServer());
|
||||
});
|
||||
|
||||
it('updateServerMessage dispatches correctly', () => {
|
||||
Dispatch.updateServerMessage();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateServerMessage());
|
||||
});
|
||||
|
||||
it('accountPasswordChange dispatches correctly', () => {
|
||||
Dispatch.accountPasswordChange();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountPasswordChange());
|
||||
});
|
||||
|
||||
it('accountEditChanged dispatches correctly', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.accountEditChanged(user);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountEditChanged(user));
|
||||
});
|
||||
|
||||
it('accountImageChanged dispatches correctly', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.accountImageChanged(user);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountImageChanged(user));
|
||||
});
|
||||
|
||||
it('directMessageSent dispatches correctly', () => {
|
||||
Dispatch.directMessageSent('Eve', 'hi');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.directMessageSent('Eve', 'hi'));
|
||||
});
|
||||
|
||||
it('getUserInfo dispatches correctly', () => {
|
||||
const userInfo = makeUser({ name: 'Frank' });
|
||||
Dispatch.getUserInfo(userInfo);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.getUserInfo(userInfo));
|
||||
});
|
||||
|
||||
it('notifyUser dispatches correctly', () => {
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
||||
Dispatch.notifyUser(notification);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
|
||||
});
|
||||
|
||||
it('serverShutdown dispatches correctly', () => {
|
||||
const data = { reason: 'maintenance', minutes: 5 };
|
||||
Dispatch.serverShutdown(data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
|
||||
});
|
||||
|
||||
it('userMessage dispatches correctly', () => {
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
||||
Dispatch.userMessage(messageData);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
|
||||
});
|
||||
|
||||
it('addToList dispatches correctly', () => {
|
||||
Dispatch.addToList('buddyList', 'Grace');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.addToList('buddyList', 'Grace'));
|
||||
});
|
||||
|
||||
it('removeFromList dispatches correctly', () => {
|
||||
Dispatch.removeFromList('buddyList', 'Hank');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.removeFromList('buddyList', 'Hank'));
|
||||
});
|
||||
|
||||
it('banFromServer dispatches correctly', () => {
|
||||
Dispatch.banFromServer('Ira');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.banFromServer('Ira'));
|
||||
});
|
||||
|
||||
it('banHistory dispatches correctly', () => {
|
||||
const history = [makeBanHistoryItem()];
|
||||
Dispatch.banHistory('Ira', history);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.banHistory('Ira', history));
|
||||
});
|
||||
|
||||
it('warnHistory dispatches correctly', () => {
|
||||
const history = [makeWarnHistoryItem()];
|
||||
Dispatch.warnHistory('Jack', history);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.warnHistory('Jack', history));
|
||||
});
|
||||
|
||||
it('warnListOptions dispatches correctly', () => {
|
||||
const list = [makeWarnListItem()];
|
||||
Dispatch.warnListOptions(list);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.warnListOptions(list));
|
||||
});
|
||||
|
||||
it('warnUser dispatches correctly', () => {
|
||||
Dispatch.warnUser('Kelly');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.warnUser('Kelly'));
|
||||
});
|
||||
|
||||
it('grantReplayAccess dispatches correctly', () => {
|
||||
Dispatch.grantReplayAccess(7, 'Moe');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.grantReplayAccess(7, 'Moe'));
|
||||
});
|
||||
|
||||
it('forceActivateUser dispatches correctly', () => {
|
||||
Dispatch.forceActivateUser('Ned', 'Moe');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.forceActivateUser('Ned', 'Moe'));
|
||||
});
|
||||
|
||||
it('getAdminNotes dispatches correctly', () => {
|
||||
Dispatch.getAdminNotes('Ned', 'notes');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.getAdminNotes('Ned', 'notes'));
|
||||
});
|
||||
|
||||
it('updateAdminNotes dispatches correctly', () => {
|
||||
Dispatch.updateAdminNotes('Ned', 'updated');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateAdminNotes('Ned', 'updated'));
|
||||
});
|
||||
|
||||
it('replayList dispatches correctly', () => {
|
||||
const list = [makeReplayMatch()];
|
||||
Dispatch.replayList(list);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.replayList(list));
|
||||
});
|
||||
|
||||
it('replayAdded dispatches correctly', () => {
|
||||
const match = makeReplayMatch();
|
||||
Dispatch.replayAdded(match);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.replayAdded(match));
|
||||
});
|
||||
|
||||
it('replayModifyMatch dispatches correctly', () => {
|
||||
Dispatch.replayModifyMatch(5, true);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.replayModifyMatch(5, true));
|
||||
});
|
||||
|
||||
it('replayDeleteMatch dispatches correctly', () => {
|
||||
Dispatch.replayDeleteMatch(5);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.replayDeleteMatch(5));
|
||||
});
|
||||
|
||||
it('backendDecks dispatches correctly', () => {
|
||||
const deckList = makeDeckList();
|
||||
Dispatch.backendDecks(deckList);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.backendDecks(deckList));
|
||||
});
|
||||
|
||||
it('deckNewDir dispatches correctly', () => {
|
||||
Dispatch.deckNewDir('a/b', 'newFolder');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.deckNewDir('a/b', 'newFolder'));
|
||||
});
|
||||
|
||||
it('deckDelDir dispatches correctly', () => {
|
||||
Dispatch.deckDelDir('a/b');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.deckDelDir('a/b'));
|
||||
});
|
||||
|
||||
it('deckUpload dispatches correctly', () => {
|
||||
const treeItem = makeDeckTreeItem();
|
||||
Dispatch.deckUpload('a/b', treeItem);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.deckUpload('a/b', treeItem));
|
||||
});
|
||||
|
||||
it('deckDelete dispatches correctly', () => {
|
||||
Dispatch.deckDelete(42);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.deckDelete(42));
|
||||
});
|
||||
});
|
||||
526
webclient/src/store/server/server.reducer.spec.ts
Normal file
526
webclient/src/store/server/server.reducer.spec.ts
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
import { StatusEnum, UserLevelFlag } from 'types';
|
||||
import { serverReducer } from './server.reducer';
|
||||
import { Types } from './server.types';
|
||||
import {
|
||||
makeBanHistoryItem,
|
||||
makeConnectOptions,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeLogItem,
|
||||
makeReplayMatch,
|
||||
makeServerState,
|
||||
makeUser,
|
||||
makeWarnHistoryItem,
|
||||
makeWarnListItem,
|
||||
} from './__mocks__/server-fixtures';
|
||||
|
||||
// ── Initialisation ───────────────────────────────────────────────────────────
|
||||
|
||||
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.status.state).toBe(StatusEnum.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('INITIALIZED → resets to initialState with initialized: true', () => {
|
||||
const state = makeServerState({ banUser: 'someone', initialized: false });
|
||||
const result = serverReducer(state, { type: Types.INITIALIZED });
|
||||
expect(result.initialized).toBe(true);
|
||||
expect(result.banUser).toBe('');
|
||||
expect(result.buddyList).toEqual([]);
|
||||
});
|
||||
|
||||
it('CLEAR_STORE → resets to initialState but preserves status', () => {
|
||||
const status = { state: StatusEnum.LOGGED_IN, description: 'logged in' };
|
||||
const state = makeServerState({ status, banUser: 'someone' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_STORE });
|
||||
expect(result.banUser).toBe('');
|
||||
expect(result.status).toEqual(status);
|
||||
expect(result.initialized).toBe(false);
|
||||
});
|
||||
|
||||
it('default → returns state unchanged for unknown action', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: '@@UNKNOWN' });
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Account & Connection ─────────────────────────────────────────────────────
|
||||
|
||||
describe('Account & Connection', () => {
|
||||
it('ACCOUNT_AWAITING_ACTIVATION → sets connectOptions from action.options', () => {
|
||||
const options = makeConnectOptions();
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
|
||||
expect(result.connectOptions).toEqual(options);
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_SUCCESS → clears connectOptions to {}', () => {
|
||||
const state = makeServerState({ connectOptions: makeConnectOptions() });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_SUCCESS });
|
||||
expect(result.connectOptions).toEqual({});
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_FAILED → clears connectOptions to {}', () => {
|
||||
const state = makeServerState({ connectOptions: makeConnectOptions() });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_FAILED });
|
||||
expect(result.connectOptions).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Server Info & Status ──────────────────────────────────────────────────────
|
||||
|
||||
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!' });
|
||||
expect(result.info.message).toBe('Welcome!');
|
||||
expect(result.info.name).toBe('Old');
|
||||
expect(result.info.version).toBe('1.0');
|
||||
});
|
||||
|
||||
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' },
|
||||
});
|
||||
expect(result.info.name).toBe('Servatrice');
|
||||
expect(result.info.version).toBe('2.9.0');
|
||||
expect(result.info.message).toBe('hi');
|
||||
});
|
||||
|
||||
it('UPDATE_STATUS → replaces state.status entirely', () => {
|
||||
const state = makeServerState();
|
||||
const status = { state: StatusEnum.LOGGED_IN, description: 'ok' };
|
||||
const result = serverReducer(state, { type: Types.UPDATE_STATUS, status });
|
||||
expect(result.status).toEqual(status);
|
||||
});
|
||||
});
|
||||
|
||||
// ── User ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('User', () => {
|
||||
it('UPDATE_USER → merges action.user into state.user', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice', userLevel: 1 }) });
|
||||
const result = serverReducer(state, {
|
||||
type: Types.UPDATE_USER,
|
||||
user: { userLevel: 8 },
|
||||
});
|
||||
expect(result.user.name).toBe('Alice');
|
||||
expect(result.user.userLevel).toBe(8);
|
||||
});
|
||||
|
||||
it('ACCOUNT_EDIT_CHANGED → merges action.user into state.user', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice' }) });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_EDIT_CHANGED, 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', () => {
|
||||
const state = makeServerState({ user: makeUser({ name: 'Alice' }) });
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_IMAGE_CHANGED, user: { country: 'US' } });
|
||||
expect(result.user.country).toBe('US');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Users List ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Users List', () => {
|
||||
it('UPDATE_USERS → replaces users list and sorts by name ASC', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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_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');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Buddy & Ignore Lists ──────────────────────────────────────────────────────
|
||||
|
||||
describe('Buddy List', () => {
|
||||
it('UPDATE_BUDDY_LIST → replaces and sorts buddy list', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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('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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ignore List', () => {
|
||||
it('UPDATE_IGNORE_LIST → replaces and sorts ignore list', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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('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');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Logs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Logs', () => {
|
||||
it('VIEW_LOGS → replaces logs entirely', () => {
|
||||
const logs = { room: [makeLogItem()], game: [], chat: [] };
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs });
|
||||
expect(result.logs).toEqual(logs);
|
||||
});
|
||||
|
||||
it('CLEAR_LOGS → resets logs to empty arrays', () => {
|
||||
const state = makeServerState({ logs: { room: [makeLogItem()], game: [], chat: [] } });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_LOGS });
|
||||
expect(result.logs.room).toEqual([]);
|
||||
expect(result.logs.game).toEqual([]);
|
||||
expect(result.logs.chat).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Messaging ─────────────────────────────────────────────────────────────────
|
||||
|
||||
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 });
|
||||
expect(result.messages['Bob']).toHaveLength(1);
|
||||
expect(result.messages['Bob'][0]).toBe(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 });
|
||||
expect(result.messages['Alice']).toHaveLength(1);
|
||||
expect(result.messages['Alice'][0]).toBe(messageData);
|
||||
});
|
||||
|
||||
it('USER_MESSAGE → appends to existing messages for that user', () => {
|
||||
const existingMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'first' };
|
||||
const state = makeServerState({
|
||||
user: makeUser({ name: 'Bob' }),
|
||||
messages: { Alice: [existingMsg] },
|
||||
});
|
||||
const newMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'second' };
|
||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
|
||||
expect(result.messages['Alice']).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── User Info & Notifications ─────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 });
|
||||
expect(result.notifications).toHaveLength(1);
|
||||
expect(result.notifications[0]).toBe(notification);
|
||||
});
|
||||
|
||||
it('SERVER_SHUTDOWN → sets serverShutdown to action.data', () => {
|
||||
const data = { reason: 'maintenance', minutes: 10 };
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.SERVER_SHUTDOWN, data });
|
||||
expect(result.serverShutdown).toBe(data);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Moderation ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Moderation', () => {
|
||||
it('BAN_FROM_SERVER → sets banUser', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.BAN_FROM_SERVER, 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('WARN_USER → sets warnUser', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.WARN_USER, 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' });
|
||||
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' });
|
||||
expect(result.adminNotes['Ira']).toBe('new');
|
||||
});
|
||||
});
|
||||
|
||||
// ── ADJUST_MOD ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ADJUST_MOD', () => {
|
||||
const baseUserLevel = UserLevelFlag.IsUser | UserLevelFlag.IsRegistered | UserLevelFlag.IsModerator | UserLevelFlag.IsJudge;
|
||||
|
||||
it('shouldBeMod=true, shouldBeJudge=true → keeps IsModerator and IsJudge bits', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
|
||||
// IsModerator(4) | IsJudge(16)
|
||||
expect(result.users[0].userLevel).toBe(20);
|
||||
});
|
||||
|
||||
it('shouldBeMod=true, shouldBeJudge=false → keeps only IsModerator bit', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||
// IsModerator(4)
|
||||
expect(result.users[0].userLevel).toBe(4);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=true → keeps only IsJudge bit', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
|
||||
// IsJudge(16)
|
||||
expect(result.users[0].userLevel).toBe(16);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=false → clears both bits', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
||||
expect(result.users[0].userLevel).toBe(0);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Replays ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Replays', () => {
|
||||
it('REPLAY_LIST → replaces replays list', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('REPLAY_ADDED → appends matchInfo to replays', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Deck Storage ──────────────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
|
||||
const subfolder = { id: 0, name: 'myDecks', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('DECK_DELETE → removes item by id from tree', () => {
|
||||
const item = makeDeckTreeItem({ id: 7 });
|
||||
const state = makeServerState({ backendDecks: { root: { items: [item] } } });
|
||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 });
|
||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('DECK_DELETE → recursively removes item nested inside a subfolder', () => {
|
||||
const nested = makeDeckTreeItem({ id: 9, name: 'nested.cod' });
|
||||
const subfolder = { id: 0, name: 'sub', file: null, folder: { items: [nested] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const result = serverReducer(state, { type: Types.DECK_DELETE, 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);
|
||||
});
|
||||
|
||||
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).toEqual({ items: [] });
|
||||
});
|
||||
|
||||
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
|
||||
const subfolder = { id: 0, name: 'parent', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { 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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → removes folder from root by name', () => {
|
||||
const subfolder = { id: 0, name: 'myDir', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => {
|
||||
const subfolder = { id: 0, name: 'keep', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => {
|
||||
const child = { id: 0, name: 'child', file: null, folder: { items: [] } };
|
||||
const parent = { id: 0, name: 'parent', file: null, folder: { items: [child] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [parent] } } });
|
||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' });
|
||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
98
webclient/src/store/server/server.selectors.spec.ts
Normal file
98
webclient/src/store/server/server.selectors.spec.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Selectors } from './server.selectors';
|
||||
import { ServerState } from './server.interfaces';
|
||||
import {
|
||||
makeDeckList,
|
||||
makeReplayMatch,
|
||||
makeServerState,
|
||||
makeUser,
|
||||
} from './__mocks__/server-fixtures';
|
||||
import { StatusEnum } from 'types';
|
||||
|
||||
function rootState(server: ServerState) {
|
||||
return { server };
|
||||
}
|
||||
|
||||
describe('Selectors', () => {
|
||||
it('getInitialized → returns initialized flag', () => {
|
||||
const state = makeServerState({ initialized: true });
|
||||
expect(Selectors.getInitialized(rootState(state))).toBe(true);
|
||||
});
|
||||
|
||||
it('getConnectOptions → returns connectOptions', () => {
|
||||
const connectOptions = { host: 'localhost', port: '4747' };
|
||||
const state = makeServerState({ connectOptions });
|
||||
expect(Selectors.getConnectOptions(rootState(state))).toBe(connectOptions);
|
||||
});
|
||||
|
||||
it('getMessage → returns info.message', () => {
|
||||
const state = makeServerState({ info: { message: 'Welcome!', name: null, version: null } });
|
||||
expect(Selectors.getMessage(rootState(state))).toBe('Welcome!');
|
||||
});
|
||||
|
||||
it('getName → returns info.name', () => {
|
||||
const state = makeServerState({ info: { message: null, name: 'Servatrice', version: null } });
|
||||
expect(Selectors.getName(rootState(state))).toBe('Servatrice');
|
||||
});
|
||||
|
||||
it('getVersion → returns info.version', () => {
|
||||
const state = makeServerState({ info: { message: null, name: null, version: '2.9.0' } });
|
||||
expect(Selectors.getVersion(rootState(state))).toBe('2.9.0');
|
||||
});
|
||||
|
||||
it('getDescription → returns status.description', () => {
|
||||
const state = makeServerState({ status: { state: StatusEnum.CONNECTED, description: 'ok' } });
|
||||
expect(Selectors.getDescription(rootState(state))).toBe('ok');
|
||||
});
|
||||
|
||||
it('getState → returns status.state', () => {
|
||||
const state = makeServerState({ status: { state: StatusEnum.LOGGED_IN, description: null } });
|
||||
expect(Selectors.getState(rootState(state))).toBe(StatusEnum.LOGGED_IN);
|
||||
});
|
||||
|
||||
it('getUser → returns user', () => {
|
||||
const user = makeUser({ name: 'Alice' });
|
||||
const state = makeServerState({ user });
|
||||
expect(Selectors.getUser(rootState(state))).toBe(user);
|
||||
});
|
||||
|
||||
it('getUsers → returns users array', () => {
|
||||
const users = [makeUser(), makeUser({ name: 'Bob' })];
|
||||
const state = makeServerState({ users });
|
||||
expect(Selectors.getUsers(rootState(state))).toBe(users);
|
||||
});
|
||||
|
||||
it('getLogs → returns logs object', () => {
|
||||
const logs = { room: [], game: [], chat: [] };
|
||||
const state = makeServerState({ logs });
|
||||
expect(Selectors.getLogs(rootState(state))).toBe(logs);
|
||||
});
|
||||
|
||||
it('getBuddyList → returns buddyList', () => {
|
||||
const buddyList = [makeUser({ name: 'Carol' })];
|
||||
const state = makeServerState({ buddyList });
|
||||
expect(Selectors.getBuddyList(rootState(state))).toBe(buddyList);
|
||||
});
|
||||
|
||||
it('getIgnoreList → returns ignoreList', () => {
|
||||
const ignoreList = [makeUser({ name: 'Dave' })];
|
||||
const state = makeServerState({ ignoreList });
|
||||
expect(Selectors.getIgnoreList(rootState(state))).toBe(ignoreList);
|
||||
});
|
||||
|
||||
it('getReplays → returns replays', () => {
|
||||
const replays = [makeReplayMatch()];
|
||||
const state = makeServerState({ replays });
|
||||
expect(Selectors.getReplays(rootState(state))).toBe(replays);
|
||||
});
|
||||
|
||||
it('getBackendDecks → returns backendDecks', () => {
|
||||
const backendDecks = makeDeckList();
|
||||
const state = makeServerState({ backendDecks });
|
||||
expect(Selectors.getBackendDecks(rootState(state))).toBe(backendDecks);
|
||||
});
|
||||
|
||||
it('getBackendDecks → returns null when not set', () => {
|
||||
const state = makeServerState({ backendDecks: null });
|
||||
expect(Selectors.getBackendDecks(rootState(state))).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue