mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-30 18:43:55 -07:00
Compare commits
7 commits
d96d5e1589
...
68e22d22bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e22d22bf | ||
|
|
98ce317ee1 | ||
|
|
559a3ff1f4 | ||
|
|
c3ae4cffd6 | ||
|
|
3001925430 | ||
|
|
367852866f | ||
|
|
74803442d2 |
172 changed files with 13978 additions and 28444 deletions
2
.github/workflows/web-build.yml
vendored
2
.github/workflows/web-build.yml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
node_version:
|
||||
- 16
|
||||
- 20
|
||||
- lts/*
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ESLINT_NO_DEV_ERRORS=true
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
DISABLE_ESLINT_PLUGIN=true
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
CI=true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {"project": ["./tsconfig.json"]},
|
||||
"parserOptions": {"ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": {"jsx": true}},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
|
|
|
|||
22
webclient/index.html
Normal file
22
webclient/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" type="text/css" href="/reset.css">
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Webatrice: A Cockatrice Web Client"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Webatrice</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
31974
webclient/package-lock.json
generated
31974
webclient/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,13 +5,13 @@
|
|||
"scripts": {
|
||||
"prebuild": "node prebuild.js",
|
||||
"prestart": "node prebuild.js",
|
||||
"build": "react-scripts build",
|
||||
"start": "react-scripts start",
|
||||
"test": "react-scripts test",
|
||||
"test:watch": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint \"./**/*.{ts,tsx}\"",
|
||||
"lint:fix": "eslint \"./**/*.{ts,tsx}\" --fix",
|
||||
"build": "vite build",
|
||||
"start": "vite",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
||||
"golden": "npm run lint && npm run test",
|
||||
"prepare": "cd .. && husky install",
|
||||
"translate": "node prebuild.js -i18nOnly"
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
"@mui/material": "^5.5.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dexie": "^3.2.2",
|
||||
"dompurify": "^3.3.3",
|
||||
"final-form": "^4.20.6",
|
||||
"final-form-set-field-touched": "^1.0.1",
|
||||
"i18next": "^22.0.4",
|
||||
|
|
@ -39,21 +40,18 @@
|
|||
"react-i18next": "^12.0.0",
|
||||
"react-redux": "^8.0.4",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
"react-window": "^1.8.6",
|
||||
"redux": "^4.1.2",
|
||||
"redux-form": "^8.3.8",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"rxjs": "^7.5.4",
|
||||
"sanitize-html": "^2.7.3"
|
||||
"rxjs": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@mui/types": "^7.1.3",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/jest-dom": "^6.4.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/jest": "29.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jquery": "^3.5.14",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/node": "18.11.7",
|
||||
|
|
@ -67,12 +65,16 @@
|
|||
"@types/redux-form": "^8.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||
"@typescript-eslint/parser": "^5.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"eslint": "^8.0.0",
|
||||
"fs-extra": "^10.0.1",
|
||||
"husky": "^8.0.1",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"jsdom": "^24.0.0",
|
||||
"typescript": "^4.6.2",
|
||||
"vite": "^5.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^1.3.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
@ -85,10 +87,5 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|less)$": "identity-obj-proxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/reset.css">
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Webatrice: A Cockatrice Web Client"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Webatrice</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "Webatrice",
|
||||
"name": "Webatrice",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
|
|
|||
48
webclient/src/api/AdminService.spec.ts
Normal file
48
webclient/src/api/AdminService.spec.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
vi.mock('websocket', () => ({
|
||||
AdminCommands: {
|
||||
adjustMod: vi.fn(),
|
||||
reloadConfig: vi.fn(),
|
||||
shutdownServer: vi.fn(),
|
||||
updateServerMessage: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { AdminService } from './AdminService';
|
||||
import { AdminCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => vi.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 @@
|
|||
vi.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
connect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
},
|
||||
webClient: {
|
||||
connectionAttemptMade: false,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.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(() => vi.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 @@
|
|||
vi.mock('websocket', () => ({
|
||||
ModeratorCommands: {
|
||||
banFromServer: vi.fn(),
|
||||
getBanHistory: vi.fn(),
|
||||
getWarnHistory: vi.fn(),
|
||||
getWarnList: vi.fn(),
|
||||
viewLogHistory: vi.fn(),
|
||||
warnUser: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { ModeratorService } from './ModeratorService';
|
||||
import { ModeratorCommands } from 'websocket';
|
||||
import { LogFilters } from 'types';
|
||||
|
||||
beforeEach(() => vi.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 @@
|
|||
vi.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
joinRoom: vi.fn(),
|
||||
},
|
||||
RoomCommands: {
|
||||
leaveRoom: vi.fn(),
|
||||
roomSay: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { RoomsService } from './RoomsService';
|
||||
import { RoomCommands, SessionCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => vi.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 @@
|
|||
vi.mock('websocket', () => ({
|
||||
SessionCommands: {
|
||||
addToBuddyList: vi.fn(),
|
||||
removeFromBuddyList: vi.fn(),
|
||||
addToIgnoreList: vi.fn(),
|
||||
removeFromIgnoreList: vi.fn(),
|
||||
accountPassword: vi.fn(),
|
||||
accountEdit: vi.fn(),
|
||||
accountImage: vi.fn(),
|
||||
message: vi.fn(),
|
||||
getUserInfo: vi.fn(),
|
||||
getGamesOfUser: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { SessionService } from './SessionService';
|
||||
import { SessionCommands } from 'websocket';
|
||||
|
||||
beforeEach(() => vi.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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -64,11 +64,13 @@ const Root = styled('div')(({ theme }) => ({
|
|||
}
|
||||
}));
|
||||
|
||||
const Login = ({ state, description, connectOptions }: LoginProps) => {
|
||||
const Login = ({ state, description }: LoginProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isConnected = AuthenticationService.isConnected(state);
|
||||
|
||||
const [pendingActivationOptions, setPendingActivationOptions] = useState<WebSocketConnectOptions | null>(null);
|
||||
|
||||
const [rememberLogin, setRememberLogin] = useState(null);
|
||||
const [dialogState, setDialogState] = useState({
|
||||
passwordResetRequestDialog: false,
|
||||
|
|
@ -97,9 +99,11 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
|
|||
useReduxEffect(() => {
|
||||
accountActivatedToast.openToast()
|
||||
closeActivateAccountDialog();
|
||||
setPendingActivationOptions(null);
|
||||
}, ServerTypes.ACCOUNT_ACTIVATION_SUCCESS, []);
|
||||
|
||||
useReduxEffect(() => {
|
||||
useReduxEffect(({ options }) => {
|
||||
setPendingActivationOptions(options);
|
||||
closeRegistrationDialog();
|
||||
openActivateAccountDialog();
|
||||
}, ServerTypes.ACCOUNT_AWAITING_ACTIVATION, []);
|
||||
|
|
@ -161,7 +165,7 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
|
|||
|
||||
const handleAccountActivationDialogSubmit = ({ token }) => {
|
||||
AuthenticationService.activateAccount({
|
||||
...connectOptions,
|
||||
...pendingActivationOptions,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
|
@ -348,13 +352,11 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
|
|||
interface LoginProps {
|
||||
state: number;
|
||||
description: string;
|
||||
connectOptions: WebSocketConnectOptions;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
state: ServerSelectors.getState(state),
|
||||
description: ServerSelectors.getDescription(state),
|
||||
connectOptions: ServerSelectors.getConnectOptions(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Login);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
|||
|
||||
useReduxEffect(() => {
|
||||
openToast()
|
||||
}, ServerTypes.REGISTRATION_SUCCES);
|
||||
}, ServerTypes.REGISTRATION_SUCCESS);
|
||||
|
||||
useReduxEffect(({ error }) => {
|
||||
setEmailError(error);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useFireOnce } from './useFireOnce';
|
|||
describe('useFireOnce hook', () => {
|
||||
test('it only fires once when button is clicked twice', async () => {
|
||||
// Mock a promise with a delay
|
||||
const onClickWithPromise = jest.fn((e) => {
|
||||
const onClickWithPromise = vi.fn((e) => {
|
||||
e.preventDefault()
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -54,7 +54,7 @@ describe('useFireOnce hook', () => {
|
|||
|
||||
test('it only fires once when form is submitted twice', async () => {
|
||||
// Mock a promise with a delay
|
||||
const onClickWithPromise = jest.fn((e) => {
|
||||
const onClickWithPromise = vi.fn((e) => {
|
||||
e.preventDefault()
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Language } from 'types';
|
|||
|
||||
class I18nBackend {
|
||||
static type: ModuleType = 'backend';
|
||||
static BASE_URL = `${process.env.PUBLIC_URL}/locales`;
|
||||
static BASE_URL = `${import.meta.env.BASE_URL}locales`;
|
||||
|
||||
read(language, namespace, callback) {
|
||||
if (!Language[language]) {
|
||||
|
|
|
|||
|
|
@ -1,256 +1,254 @@
|
|||
// Remove !file-loader! once the following is no longer an issue
|
||||
// https://github.com/facebook/create-react-app/issues/11770
|
||||
import ad from '!file-loader!./ad.svg';
|
||||
import ae from '!file-loader!./ae.svg';
|
||||
import af from '!file-loader!./af.svg';
|
||||
import ag from '!file-loader!./ag.svg';
|
||||
import ai from '!file-loader!./ai.svg';
|
||||
import al from '!file-loader!./al.svg';
|
||||
import am from '!file-loader!./am.svg';
|
||||
import ao from '!file-loader!./ao.svg';
|
||||
import aq from '!file-loader!./aq.svg';
|
||||
import ar from '!file-loader!./ar.svg';
|
||||
import as from '!file-loader!./as.svg';
|
||||
import at from '!file-loader!./at.svg';
|
||||
import au from '!file-loader!./au.svg';
|
||||
import aw from '!file-loader!./aw.svg';
|
||||
import ax from '!file-loader!./ax.svg';
|
||||
import az from '!file-loader!./az.svg';
|
||||
import ba from '!file-loader!./ba.svg';
|
||||
import bb from '!file-loader!./bb.svg';
|
||||
import bd from '!file-loader!./bd.svg';
|
||||
import be from '!file-loader!./be.svg';
|
||||
import bf from '!file-loader!./bf.svg';
|
||||
import bg from '!file-loader!./bg.svg';
|
||||
import bh from '!file-loader!./bh.svg';
|
||||
import bi from '!file-loader!./bi.svg';
|
||||
import bj from '!file-loader!./bj.svg';
|
||||
import bl from '!file-loader!./bl.svg';
|
||||
import bm from '!file-loader!./bm.svg';
|
||||
import bn from '!file-loader!./bn.svg';
|
||||
import bo from '!file-loader!./bo.svg';
|
||||
import bq from '!file-loader!./bq.svg';
|
||||
import br from '!file-loader!./br.svg';
|
||||
import bs from '!file-loader!./bs.svg';
|
||||
import bt from '!file-loader!./bt.svg';
|
||||
import bv from '!file-loader!./bv.svg';
|
||||
import bw from '!file-loader!./bw.svg';
|
||||
import by from '!file-loader!./by.svg';
|
||||
import bz from '!file-loader!./bz.svg';
|
||||
import ca from '!file-loader!./ca.svg';
|
||||
import cc from '!file-loader!./cc.svg';
|
||||
import cd from '!file-loader!./cd.svg';
|
||||
import cf from '!file-loader!./cf.svg';
|
||||
import cg from '!file-loader!./cg.svg';
|
||||
import ch from '!file-loader!./ch.svg';
|
||||
import ci from '!file-loader!./ci.svg';
|
||||
import ck from '!file-loader!./ck.svg';
|
||||
import cl from '!file-loader!./cl.svg';
|
||||
import cm from '!file-loader!./cm.svg';
|
||||
import cn from '!file-loader!./cn.svg';
|
||||
import co from '!file-loader!./co.svg';
|
||||
import cr from '!file-loader!./cr.svg';
|
||||
import cu from '!file-loader!./cu.svg';
|
||||
import cv from '!file-loader!./cv.svg';
|
||||
import cw from '!file-loader!./cw.svg';
|
||||
import cx from '!file-loader!./cx.svg';
|
||||
import cy from '!file-loader!./cy.svg';
|
||||
import cz from '!file-loader!./cz.svg';
|
||||
import de from '!file-loader!./de.svg';
|
||||
import dj from '!file-loader!./dj.svg';
|
||||
import dk from '!file-loader!./dk.svg';
|
||||
import dm from '!file-loader!./dm.svg';
|
||||
import _do from '!file-loader!./do.svg';
|
||||
import dz from '!file-loader!./dz.svg';
|
||||
import ec from '!file-loader!./ec.svg';
|
||||
import ee from '!file-loader!./ee.svg';
|
||||
import eg from '!file-loader!./eg.svg';
|
||||
import eh from '!file-loader!./eh.svg';
|
||||
import er from '!file-loader!./er.svg';
|
||||
import es from '!file-loader!./es.svg';
|
||||
import et from '!file-loader!./et.svg';
|
||||
import eu from '!file-loader!./eu.svg';
|
||||
import fi from '!file-loader!./fi.svg';
|
||||
import fj from '!file-loader!./fj.svg';
|
||||
import fk from '!file-loader!./fk.svg';
|
||||
import fm from '!file-loader!./fm.svg';
|
||||
import fo from '!file-loader!./fo.svg';
|
||||
import fr from '!file-loader!./fr.svg';
|
||||
import ga from '!file-loader!./ga.svg';
|
||||
import gb from '!file-loader!./gb.svg';
|
||||
import gd from '!file-loader!./gd.svg';
|
||||
import ge from '!file-loader!./ge.svg';
|
||||
import gf from '!file-loader!./gf.svg';
|
||||
import gg from '!file-loader!./gg.svg';
|
||||
import gh from '!file-loader!./gh.svg';
|
||||
import gi from '!file-loader!./gi.svg';
|
||||
import gl from '!file-loader!./gl.svg';
|
||||
import gm from '!file-loader!./gm.svg';
|
||||
import gn from '!file-loader!./gn.svg';
|
||||
import gp from '!file-loader!./gp.svg';
|
||||
import gq from '!file-loader!./gq.svg';
|
||||
import gr from '!file-loader!./gr.svg';
|
||||
import gs from '!file-loader!./gs.svg';
|
||||
import gt from '!file-loader!./gt.svg';
|
||||
import gu from '!file-loader!./gu.svg';
|
||||
import gw from '!file-loader!./gw.svg';
|
||||
import gy from '!file-loader!./gy.svg';
|
||||
import hk from '!file-loader!./hk.svg';
|
||||
import hm from '!file-loader!./hm.svg';
|
||||
import hn from '!file-loader!./hn.svg';
|
||||
import hr from '!file-loader!./hr.svg';
|
||||
import ht from '!file-loader!./ht.svg';
|
||||
import hu from '!file-loader!./hu.svg';
|
||||
import id from '!file-loader!./id.svg';
|
||||
import ie from '!file-loader!./ie.svg';
|
||||
import il from '!file-loader!./il.svg';
|
||||
import im from '!file-loader!./im.svg';
|
||||
import _in from '!file-loader!./in.svg';
|
||||
import io from '!file-loader!./io.svg';
|
||||
import iq from '!file-loader!./iq.svg';
|
||||
import ir from '!file-loader!./ir.svg';
|
||||
import is from '!file-loader!./is.svg';
|
||||
import it from '!file-loader!./it.svg';
|
||||
import je from '!file-loader!./je.svg';
|
||||
import jm from '!file-loader!./jm.svg';
|
||||
import jo from '!file-loader!./jo.svg';
|
||||
import jp from '!file-loader!./jp.svg';
|
||||
import ke from '!file-loader!./ke.svg';
|
||||
import kg from '!file-loader!./kg.svg';
|
||||
import kh from '!file-loader!./kh.svg';
|
||||
import ki from '!file-loader!./ki.svg';
|
||||
import km from '!file-loader!./km.svg';
|
||||
import kn from '!file-loader!./kn.svg';
|
||||
import kp from '!file-loader!./kp.svg';
|
||||
import kr from '!file-loader!./kr.svg';
|
||||
import kw from '!file-loader!./kw.svg';
|
||||
import ky from '!file-loader!./ky.svg';
|
||||
import kz from '!file-loader!./kz.svg';
|
||||
import la from '!file-loader!./la.svg';
|
||||
import lb from '!file-loader!./lb.svg';
|
||||
import lc from '!file-loader!./lc.svg';
|
||||
import li from '!file-loader!./li.svg';
|
||||
import lk from '!file-loader!./lk.svg';
|
||||
import lr from '!file-loader!./lr.svg';
|
||||
import ls from '!file-loader!./ls.svg';
|
||||
import lt from '!file-loader!./lt.svg';
|
||||
import lu from '!file-loader!./lu.svg';
|
||||
import lv from '!file-loader!./lv.svg';
|
||||
import ly from '!file-loader!./ly.svg';
|
||||
import ma from '!file-loader!./ma.svg';
|
||||
import mc from '!file-loader!./mc.svg';
|
||||
import md from '!file-loader!./md.svg';
|
||||
import me from '!file-loader!./me.svg';
|
||||
import mf from '!file-loader!./mf.svg';
|
||||
import mg from '!file-loader!./mg.svg';
|
||||
import mh from '!file-loader!./mh.svg';
|
||||
import mk from '!file-loader!./mk.svg';
|
||||
import ml from '!file-loader!./ml.svg';
|
||||
import mm from '!file-loader!./mm.svg';
|
||||
import mn from '!file-loader!./mn.svg';
|
||||
import mo from '!file-loader!./mo.svg';
|
||||
import mp from '!file-loader!./mp.svg';
|
||||
import mq from '!file-loader!./mq.svg';
|
||||
import mr from '!file-loader!./mr.svg';
|
||||
import ms from '!file-loader!./ms.svg';
|
||||
import mt from '!file-loader!./mt.svg';
|
||||
import mu from '!file-loader!./mu.svg';
|
||||
import mv from '!file-loader!./mv.svg';
|
||||
import mw from '!file-loader!./mw.svg';
|
||||
import mx from '!file-loader!./mx.svg';
|
||||
import my from '!file-loader!./my.svg';
|
||||
import mz from '!file-loader!./mz.svg';
|
||||
import na from '!file-loader!./na.svg';
|
||||
import nc from '!file-loader!./nc.svg';
|
||||
import ne from '!file-loader!./ne.svg';
|
||||
import nf from '!file-loader!./nf.svg';
|
||||
import ng from '!file-loader!./ng.svg';
|
||||
import ni from '!file-loader!./ni.svg';
|
||||
import nl from '!file-loader!./nl.svg';
|
||||
import no from '!file-loader!./no.svg';
|
||||
import np from '!file-loader!./np.svg';
|
||||
import nr from '!file-loader!./nr.svg';
|
||||
import nu from '!file-loader!./nu.svg';
|
||||
import nz from '!file-loader!./nz.svg';
|
||||
import om from '!file-loader!./om.svg';
|
||||
import pa from '!file-loader!./pa.svg';
|
||||
import pe from '!file-loader!./pe.svg';
|
||||
import pf from '!file-loader!./pf.svg';
|
||||
import pg from '!file-loader!./pg.svg';
|
||||
import ph from '!file-loader!./ph.svg';
|
||||
import pk from '!file-loader!./pk.svg';
|
||||
import pl from '!file-loader!./pl.svg';
|
||||
import pm from '!file-loader!./pm.svg';
|
||||
import pn from '!file-loader!./pn.svg';
|
||||
import pr from '!file-loader!./pr.svg';
|
||||
import ps from '!file-loader!./ps.svg';
|
||||
import pt from '!file-loader!./pt.svg';
|
||||
import pw from '!file-loader!./pw.svg';
|
||||
import py from '!file-loader!./py.svg';
|
||||
import qa from '!file-loader!./qa.svg';
|
||||
import re from '!file-loader!./re.svg';
|
||||
import ro from '!file-loader!./ro.svg';
|
||||
import rs from '!file-loader!./rs.svg';
|
||||
import ru from '!file-loader!./ru.svg';
|
||||
import rw from '!file-loader!./rw.svg';
|
||||
import sa from '!file-loader!./sa.svg';
|
||||
import sb from '!file-loader!./sb.svg';
|
||||
import sc from '!file-loader!./sc.svg';
|
||||
import sd from '!file-loader!./sd.svg';
|
||||
import se from '!file-loader!./se.svg';
|
||||
import sg from '!file-loader!./sg.svg';
|
||||
import sh from '!file-loader!./sh.svg';
|
||||
import si from '!file-loader!./si.svg';
|
||||
import sj from '!file-loader!./sj.svg';
|
||||
import sk from '!file-loader!./sk.svg';
|
||||
import sl from '!file-loader!./sl.svg';
|
||||
import sm from '!file-loader!./sm.svg';
|
||||
import sn from '!file-loader!./sn.svg';
|
||||
import so from '!file-loader!./so.svg';
|
||||
import sr from '!file-loader!./sr.svg';
|
||||
import ss from '!file-loader!./ss.svg';
|
||||
import st from '!file-loader!./st.svg';
|
||||
import sv from '!file-loader!./sv.svg';
|
||||
import sx from '!file-loader!./sx.svg';
|
||||
import sy from '!file-loader!./sy.svg';
|
||||
import sz from '!file-loader!./sz.svg';
|
||||
import tc from '!file-loader!./tc.svg';
|
||||
import td from '!file-loader!./td.svg';
|
||||
import tf from '!file-loader!./tf.svg';
|
||||
import tg from '!file-loader!./tg.svg';
|
||||
import th from '!file-loader!./th.svg';
|
||||
import tj from '!file-loader!./tj.svg';
|
||||
import tk from '!file-loader!./tk.svg';
|
||||
import tl from '!file-loader!./tl.svg';
|
||||
import tm from '!file-loader!./tm.svg';
|
||||
import tn from '!file-loader!./tn.svg';
|
||||
import to from '!file-loader!./to.svg';
|
||||
import tr from '!file-loader!./tr.svg';
|
||||
import tt from '!file-loader!./tt.svg';
|
||||
import tv from '!file-loader!./tv.svg';
|
||||
import tw from '!file-loader!./tw.svg';
|
||||
import tz from '!file-loader!./tz.svg';
|
||||
import ua from '!file-loader!./ua.svg';
|
||||
import ug from '!file-loader!./ug.svg';
|
||||
import um from '!file-loader!./um.svg';
|
||||
import us from '!file-loader!./us.svg';
|
||||
import uy from '!file-loader!./uy.svg';
|
||||
import uz from '!file-loader!./uz.svg';
|
||||
import va from '!file-loader!./va.svg';
|
||||
import vc from '!file-loader!./vc.svg';
|
||||
import ve from '!file-loader!./ve.svg';
|
||||
import vg from '!file-loader!./vg.svg';
|
||||
import vi from '!file-loader!./vi.svg';
|
||||
import vn from '!file-loader!./vn.svg';
|
||||
import vu from '!file-loader!./vu.svg';
|
||||
import wf from '!file-loader!./wf.svg';
|
||||
import ws from '!file-loader!./ws.svg';
|
||||
import xk from '!file-loader!./xk.svg';
|
||||
import ye from '!file-loader!./ye.svg';
|
||||
import yt from '!file-loader!./yt.svg';
|
||||
import za from '!file-loader!./za.svg';
|
||||
import zm from '!file-loader!./zm.svg';
|
||||
import zw from '!file-loader!./zw.svg';
|
||||
import ad from './ad.svg';
|
||||
import ae from './ae.svg';
|
||||
import af from './af.svg';
|
||||
import ag from './ag.svg';
|
||||
import ai from './ai.svg';
|
||||
import al from './al.svg';
|
||||
import am from './am.svg';
|
||||
import ao from './ao.svg';
|
||||
import aq from './aq.svg';
|
||||
import ar from './ar.svg';
|
||||
import as from './as.svg';
|
||||
import at from './at.svg';
|
||||
import au from './au.svg';
|
||||
import aw from './aw.svg';
|
||||
import ax from './ax.svg';
|
||||
import az from './az.svg';
|
||||
import ba from './ba.svg';
|
||||
import bb from './bb.svg';
|
||||
import bd from './bd.svg';
|
||||
import be from './be.svg';
|
||||
import bf from './bf.svg';
|
||||
import bg from './bg.svg';
|
||||
import bh from './bh.svg';
|
||||
import bi from './bi.svg';
|
||||
import bj from './bj.svg';
|
||||
import bl from './bl.svg';
|
||||
import bm from './bm.svg';
|
||||
import bn from './bn.svg';
|
||||
import bo from './bo.svg';
|
||||
import bq from './bq.svg';
|
||||
import br from './br.svg';
|
||||
import bs from './bs.svg';
|
||||
import bt from './bt.svg';
|
||||
import bv from './bv.svg';
|
||||
import bw from './bw.svg';
|
||||
import by from './by.svg';
|
||||
import bz from './bz.svg';
|
||||
import ca from './ca.svg';
|
||||
import cc from './cc.svg';
|
||||
import cd from './cd.svg';
|
||||
import cf from './cf.svg';
|
||||
import cg from './cg.svg';
|
||||
import ch from './ch.svg';
|
||||
import ci from './ci.svg';
|
||||
import ck from './ck.svg';
|
||||
import cl from './cl.svg';
|
||||
import cm from './cm.svg';
|
||||
import cn from './cn.svg';
|
||||
import co from './co.svg';
|
||||
import cr from './cr.svg';
|
||||
import cu from './cu.svg';
|
||||
import cv from './cv.svg';
|
||||
import cw from './cw.svg';
|
||||
import cx from './cx.svg';
|
||||
import cy from './cy.svg';
|
||||
import cz from './cz.svg';
|
||||
import de from './de.svg';
|
||||
import dj from './dj.svg';
|
||||
import dk from './dk.svg';
|
||||
import dm from './dm.svg';
|
||||
import _do from './do.svg';
|
||||
import dz from './dz.svg';
|
||||
import ec from './ec.svg';
|
||||
import ee from './ee.svg';
|
||||
import eg from './eg.svg';
|
||||
import eh from './eh.svg';
|
||||
import er from './er.svg';
|
||||
import es from './es.svg';
|
||||
import et from './et.svg';
|
||||
import eu from './eu.svg';
|
||||
import fi from './fi.svg';
|
||||
import fj from './fj.svg';
|
||||
import fk from './fk.svg';
|
||||
import fm from './fm.svg';
|
||||
import fo from './fo.svg';
|
||||
import fr from './fr.svg';
|
||||
import ga from './ga.svg';
|
||||
import gb from './gb.svg';
|
||||
import gd from './gd.svg';
|
||||
import ge from './ge.svg';
|
||||
import gf from './gf.svg';
|
||||
import gg from './gg.svg';
|
||||
import gh from './gh.svg';
|
||||
import gi from './gi.svg';
|
||||
import gl from './gl.svg';
|
||||
import gm from './gm.svg';
|
||||
import gn from './gn.svg';
|
||||
import gp from './gp.svg';
|
||||
import gq from './gq.svg';
|
||||
import gr from './gr.svg';
|
||||
import gs from './gs.svg';
|
||||
import gt from './gt.svg';
|
||||
import gu from './gu.svg';
|
||||
import gw from './gw.svg';
|
||||
import gy from './gy.svg';
|
||||
import hk from './hk.svg';
|
||||
import hm from './hm.svg';
|
||||
import hn from './hn.svg';
|
||||
import hr from './hr.svg';
|
||||
import ht from './ht.svg';
|
||||
import hu from './hu.svg';
|
||||
import id from './id.svg';
|
||||
import ie from './ie.svg';
|
||||
import il from './il.svg';
|
||||
import im from './im.svg';
|
||||
import _in from './in.svg';
|
||||
import io from './io.svg';
|
||||
import iq from './iq.svg';
|
||||
import ir from './ir.svg';
|
||||
import is from './is.svg';
|
||||
import it from './it.svg';
|
||||
import je from './je.svg';
|
||||
import jm from './jm.svg';
|
||||
import jo from './jo.svg';
|
||||
import jp from './jp.svg';
|
||||
import ke from './ke.svg';
|
||||
import kg from './kg.svg';
|
||||
import kh from './kh.svg';
|
||||
import ki from './ki.svg';
|
||||
import km from './km.svg';
|
||||
import kn from './kn.svg';
|
||||
import kp from './kp.svg';
|
||||
import kr from './kr.svg';
|
||||
import kw from './kw.svg';
|
||||
import ky from './ky.svg';
|
||||
import kz from './kz.svg';
|
||||
import la from './la.svg';
|
||||
import lb from './lb.svg';
|
||||
import lc from './lc.svg';
|
||||
import li from './li.svg';
|
||||
import lk from './lk.svg';
|
||||
import lr from './lr.svg';
|
||||
import ls from './ls.svg';
|
||||
import lt from './lt.svg';
|
||||
import lu from './lu.svg';
|
||||
import lv from './lv.svg';
|
||||
import ly from './ly.svg';
|
||||
import ma from './ma.svg';
|
||||
import mc from './mc.svg';
|
||||
import md from './md.svg';
|
||||
import me from './me.svg';
|
||||
import mf from './mf.svg';
|
||||
import mg from './mg.svg';
|
||||
import mh from './mh.svg';
|
||||
import mk from './mk.svg';
|
||||
import ml from './ml.svg';
|
||||
import mm from './mm.svg';
|
||||
import mn from './mn.svg';
|
||||
import mo from './mo.svg';
|
||||
import mp from './mp.svg';
|
||||
import mq from './mq.svg';
|
||||
import mr from './mr.svg';
|
||||
import ms from './ms.svg';
|
||||
import mt from './mt.svg';
|
||||
import mu from './mu.svg';
|
||||
import mv from './mv.svg';
|
||||
import mw from './mw.svg';
|
||||
import mx from './mx.svg';
|
||||
import my from './my.svg';
|
||||
import mz from './mz.svg';
|
||||
import na from './na.svg';
|
||||
import nc from './nc.svg';
|
||||
import ne from './ne.svg';
|
||||
import nf from './nf.svg';
|
||||
import ng from './ng.svg';
|
||||
import ni from './ni.svg';
|
||||
import nl from './nl.svg';
|
||||
import no from './no.svg';
|
||||
import np from './np.svg';
|
||||
import nr from './nr.svg';
|
||||
import nu from './nu.svg';
|
||||
import nz from './nz.svg';
|
||||
import om from './om.svg';
|
||||
import pa from './pa.svg';
|
||||
import pe from './pe.svg';
|
||||
import pf from './pf.svg';
|
||||
import pg from './pg.svg';
|
||||
import ph from './ph.svg';
|
||||
import pk from './pk.svg';
|
||||
import pl from './pl.svg';
|
||||
import pm from './pm.svg';
|
||||
import pn from './pn.svg';
|
||||
import pr from './pr.svg';
|
||||
import ps from './ps.svg';
|
||||
import pt from './pt.svg';
|
||||
import pw from './pw.svg';
|
||||
import py from './py.svg';
|
||||
import qa from './qa.svg';
|
||||
import re from './re.svg';
|
||||
import ro from './ro.svg';
|
||||
import rs from './rs.svg';
|
||||
import ru from './ru.svg';
|
||||
import rw from './rw.svg';
|
||||
import sa from './sa.svg';
|
||||
import sb from './sb.svg';
|
||||
import sc from './sc.svg';
|
||||
import sd from './sd.svg';
|
||||
import se from './se.svg';
|
||||
import sg from './sg.svg';
|
||||
import sh from './sh.svg';
|
||||
import si from './si.svg';
|
||||
import sj from './sj.svg';
|
||||
import sk from './sk.svg';
|
||||
import sl from './sl.svg';
|
||||
import sm from './sm.svg';
|
||||
import sn from './sn.svg';
|
||||
import so from './so.svg';
|
||||
import sr from './sr.svg';
|
||||
import ss from './ss.svg';
|
||||
import st from './st.svg';
|
||||
import sv from './sv.svg';
|
||||
import sx from './sx.svg';
|
||||
import sy from './sy.svg';
|
||||
import sz from './sz.svg';
|
||||
import tc from './tc.svg';
|
||||
import td from './td.svg';
|
||||
import tf from './tf.svg';
|
||||
import tg from './tg.svg';
|
||||
import th from './th.svg';
|
||||
import tj from './tj.svg';
|
||||
import tk from './tk.svg';
|
||||
import tl from './tl.svg';
|
||||
import tm from './tm.svg';
|
||||
import tn from './tn.svg';
|
||||
import to from './to.svg';
|
||||
import tr from './tr.svg';
|
||||
import tt from './tt.svg';
|
||||
import tv from './tv.svg';
|
||||
import tw from './tw.svg';
|
||||
import tz from './tz.svg';
|
||||
import ua from './ua.svg';
|
||||
import ug from './ug.svg';
|
||||
import um from './um.svg';
|
||||
import us from './us.svg';
|
||||
import uy from './uy.svg';
|
||||
import uz from './uz.svg';
|
||||
import va from './va.svg';
|
||||
import vc from './vc.svg';
|
||||
import ve from './ve.svg';
|
||||
import vg from './vg.svg';
|
||||
import vi from './vi.svg';
|
||||
import vn from './vn.svg';
|
||||
import vu from './vu.svg';
|
||||
import wf from './wf.svg';
|
||||
import ws from './ws.svg';
|
||||
import xk from './xk.svg';
|
||||
import ye from './ye.svg';
|
||||
import yt from './yt.svg';
|
||||
import za from './za.svg';
|
||||
import zm from './zm.svg';
|
||||
import zw from './zw.svg';
|
||||
|
||||
export const Countries = {
|
||||
ad,
|
||||
|
|
|
|||
1
webclient/src/react-app-env.d.ts
vendored
1
webclient/src/react-app-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import protobuf from 'protobufjs';
|
||||
|
||||
// ensure jest-dom is always available during testing to cut down on boilerplate
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
class MockProtobufRoot {
|
||||
load() {}
|
||||
|
|
|
|||
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;
|
||||
|
|
|
|||
124
webclient/src/store/game/__mocks__/fixtures.ts
Normal file
124
webclient/src/store/game/__mocks__/fixtures.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { ArrowInfo, CardInfo, CounterInfo, PlayerProperties } from 'types';
|
||||
import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces';
|
||||
|
||||
export function makeCard(overrides: Partial<CardInfo> = {}): CardInfo {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Test Card',
|
||||
x: 0,
|
||||
y: 0,
|
||||
faceDown: false,
|
||||
tapped: false,
|
||||
attacking: false,
|
||||
color: '',
|
||||
pt: '',
|
||||
annotation: '',
|
||||
destroyOnZoneChange: false,
|
||||
doesntUntap: false,
|
||||
counterList: [],
|
||||
attachPlayerId: -1,
|
||||
attachZone: '',
|
||||
attachCardId: -1,
|
||||
providerId: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeCounter(overrides: Partial<CounterInfo> = {}): CounterInfo {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Life',
|
||||
counterColor: { r: 0, g: 0, b: 0, a: 255 },
|
||||
radius: 1,
|
||||
count: 20,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeArrow(overrides: Partial<ArrowInfo> = {}): ArrowInfo {
|
||||
return {
|
||||
id: 1,
|
||||
startPlayerId: 1,
|
||||
startZone: 'table',
|
||||
startCardId: 1,
|
||||
targetPlayerId: 1,
|
||||
targetZone: 'table',
|
||||
targetCardId: 2,
|
||||
arrowColor: { r: 255, g: 0, b: 0, a: 255 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
|
||||
return {
|
||||
name: 'hand',
|
||||
type: 1,
|
||||
withCoords: false,
|
||||
cardCount: 0,
|
||||
cards: [],
|
||||
alwaysRevealTopCard: false,
|
||||
alwaysLookAtTopCard: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makePlayerProperties(overrides: Partial<PlayerProperties> = {}): PlayerProperties {
|
||||
return {
|
||||
playerId: 1,
|
||||
userInfo: null,
|
||||
spectator: false,
|
||||
conceded: false,
|
||||
readyStart: false,
|
||||
deckHash: '',
|
||||
pingSeconds: 0,
|
||||
sideboardLocked: false,
|
||||
judge: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEntry {
|
||||
return {
|
||||
properties: makePlayerProperties(),
|
||||
deckList: '',
|
||||
zones: {
|
||||
hand: makeZoneEntry({ name: 'hand' }),
|
||||
deck: makeZoneEntry({ name: 'deck' }),
|
||||
},
|
||||
counters: {},
|
||||
arrows: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeGameEntry(overrides: Partial<GameEntry> = {}): GameEntry {
|
||||
return {
|
||||
gameId: 1,
|
||||
roomId: 1,
|
||||
description: 'Test Game',
|
||||
hostId: 1,
|
||||
localPlayerId: 1,
|
||||
spectator: false,
|
||||
judge: false,
|
||||
resuming: false,
|
||||
started: false,
|
||||
activePlayerId: 0,
|
||||
activePhase: 0,
|
||||
secondsElapsed: 0,
|
||||
reversed: false,
|
||||
players: {
|
||||
1: makePlayerEntry(),
|
||||
},
|
||||
messages: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeState(overrides: Partial<GamesState> = {}): GamesState {
|
||||
return {
|
||||
games: {
|
||||
1: makeGameEntry(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
175
webclient/src/store/game/game.actions.spec.ts
Normal file
175
webclient/src/store/game/game.actions.spec.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { Actions } from './game.actions';
|
||||
import { Types } from './game.types';
|
||||
import {
|
||||
makeArrow,
|
||||
makeCard,
|
||||
makeCounter,
|
||||
makeGameEntry,
|
||||
makePlayerProperties,
|
||||
makeZoneEntry,
|
||||
} from './__mocks__/fixtures';
|
||||
|
||||
describe('Actions', () => {
|
||||
it('clearStore', () => {
|
||||
expect(Actions.clearStore()).toEqual({ type: Types.CLEAR_STORE });
|
||||
});
|
||||
|
||||
it('gameJoined', () => {
|
||||
const entry = makeGameEntry();
|
||||
expect(Actions.gameJoined(1, entry)).toEqual({ type: Types.GAME_JOINED, gameId: 1, gameEntry: entry });
|
||||
});
|
||||
|
||||
it('gameLeft', () => {
|
||||
expect(Actions.gameLeft(2)).toEqual({ type: Types.GAME_LEFT, gameId: 2 });
|
||||
});
|
||||
|
||||
it('gameClosed', () => {
|
||||
expect(Actions.gameClosed(3)).toEqual({ type: Types.GAME_CLOSED, gameId: 3 });
|
||||
});
|
||||
|
||||
it('gameHostChanged', () => {
|
||||
expect(Actions.gameHostChanged(1, 7)).toEqual({ type: Types.GAME_HOST_CHANGED, gameId: 1, hostId: 7 });
|
||||
});
|
||||
|
||||
it('gameStateChanged', () => {
|
||||
const data = { playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0 };
|
||||
expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data });
|
||||
});
|
||||
|
||||
it('playerJoined', () => {
|
||||
const props = makePlayerProperties();
|
||||
expect(Actions.playerJoined(1, props)).toEqual({ type: Types.PLAYER_JOINED, gameId: 1, playerProperties: props });
|
||||
});
|
||||
|
||||
it('playerLeft', () => {
|
||||
expect(Actions.playerLeft(1, 2, 3)).toEqual({ type: Types.PLAYER_LEFT, gameId: 1, playerId: 2, reason: 3 });
|
||||
});
|
||||
|
||||
it('playerPropertiesChanged', () => {
|
||||
const props = makePlayerProperties();
|
||||
expect(Actions.playerPropertiesChanged(1, 2, props)).toEqual({
|
||||
type: Types.PLAYER_PROPERTIES_CHANGED,
|
||||
gameId: 1,
|
||||
playerId: 2,
|
||||
properties: props,
|
||||
});
|
||||
});
|
||||
|
||||
it('kicked', () => {
|
||||
expect(Actions.kicked(1)).toEqual({ type: Types.KICKED, gameId: 1 });
|
||||
});
|
||||
|
||||
it('cardMoved', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardFlipped', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardDestroyed', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardAttached', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('tokenCreated', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardAttrChanged', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardCounterChanged', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('arrowCreated', () => {
|
||||
const arrow = makeArrow();
|
||||
const data = { arrowInfo: arrow };
|
||||
expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('arrowDeleted', () => {
|
||||
const data = { arrowId: 3 };
|
||||
expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('counterCreated', () => {
|
||||
const counter = makeCounter();
|
||||
const data = { counterInfo: counter };
|
||||
expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('counterSet', () => {
|
||||
const data = { counterId: 1, value: 10 };
|
||||
expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('counterDeleted', () => {
|
||||
const data = { counterId: 1 };
|
||||
expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardsDrawn', () => {
|
||||
const card = makeCard();
|
||||
const data = { number: 2, cards: [card] };
|
||||
expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('cardsRevealed', () => {
|
||||
const data = { zoneName: 'hand', cards: [] } as any;
|
||||
expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('zoneShuffled', () => {
|
||||
const data = { zoneName: 'deck', start: 0, end: 39 };
|
||||
expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('dieRolled', () => {
|
||||
const data = { sides: 6, value: 4, values: [4] };
|
||||
expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('activePlayerSet', () => {
|
||||
expect(Actions.activePlayerSet(1, 3)).toEqual({ type: Types.ACTIVE_PLAYER_SET, gameId: 1, activePlayerId: 3 });
|
||||
});
|
||||
|
||||
it('activePhaseSet', () => {
|
||||
expect(Actions.activePhaseSet(1, 2)).toEqual({ type: Types.ACTIVE_PHASE_SET, gameId: 1, phase: 2 });
|
||||
});
|
||||
|
||||
it('turnReversed', () => {
|
||||
expect(Actions.turnReversed(1, true)).toEqual({ type: Types.TURN_REVERSED, gameId: 1, reversed: true });
|
||||
});
|
||||
|
||||
it('zoneDumped', () => {
|
||||
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
|
||||
expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data });
|
||||
});
|
||||
|
||||
it('zonePropertiesChanged', () => {
|
||||
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
|
||||
expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({
|
||||
type: Types.ZONE_PROPERTIES_CHANGED,
|
||||
gameId: 1,
|
||||
playerId: 2,
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
it('gameSay', () => {
|
||||
expect(Actions.gameSay(1, 2, 'hello')).toEqual({ type: Types.GAME_SAY, gameId: 1, playerId: 2, message: 'hello' });
|
||||
});
|
||||
});
|
||||
234
webclient/src/store/game/game.actions.ts
Normal file
234
webclient/src/store/game/game.actions.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import {
|
||||
AttachCardData,
|
||||
ChangeZonePropertiesData,
|
||||
CreateArrowData,
|
||||
CreateCounterData,
|
||||
CreateTokenData,
|
||||
DelCounterData,
|
||||
DeleteArrowData,
|
||||
DestroyCardData,
|
||||
DrawCardsData,
|
||||
DumpZoneData,
|
||||
FlipCardData,
|
||||
GameStateChangedData,
|
||||
MoveCardData,
|
||||
PlayerProperties,
|
||||
RevealCardsData,
|
||||
RollDieData,
|
||||
SetCardAttrData,
|
||||
SetCardCounterData,
|
||||
SetCounterData,
|
||||
ShuffleData,
|
||||
} from 'types';
|
||||
import { GameEntry } from './game.interfaces';
|
||||
import { Types } from './game.types';
|
||||
|
||||
export const Actions = {
|
||||
clearStore: () => ({
|
||||
type: Types.CLEAR_STORE,
|
||||
}),
|
||||
|
||||
gameJoined: (gameId: number, gameEntry: GameEntry) => ({
|
||||
type: Types.GAME_JOINED,
|
||||
gameId,
|
||||
gameEntry,
|
||||
}),
|
||||
|
||||
gameLeft: (gameId: number) => ({
|
||||
type: Types.GAME_LEFT,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
gameClosed: (gameId: number) => ({
|
||||
type: Types.GAME_CLOSED,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
gameHostChanged: (gameId: number, hostId: number) => ({
|
||||
type: Types.GAME_HOST_CHANGED,
|
||||
gameId,
|
||||
hostId,
|
||||
}),
|
||||
|
||||
gameStateChanged: (gameId: number, data: GameStateChangedData) => ({
|
||||
type: Types.GAME_STATE_CHANGED,
|
||||
gameId,
|
||||
data,
|
||||
}),
|
||||
|
||||
playerJoined: (gameId: number, playerProperties: PlayerProperties) => ({
|
||||
type: Types.PLAYER_JOINED,
|
||||
gameId,
|
||||
playerProperties,
|
||||
}),
|
||||
|
||||
playerLeft: (gameId: number, playerId: number, reason: number) => ({
|
||||
type: Types.PLAYER_LEFT,
|
||||
gameId,
|
||||
playerId,
|
||||
reason,
|
||||
}),
|
||||
|
||||
playerPropertiesChanged: (gameId: number, playerId: number, properties: PlayerProperties) => ({
|
||||
type: Types.PLAYER_PROPERTIES_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
properties,
|
||||
}),
|
||||
|
||||
kicked: (gameId: number) => ({
|
||||
type: Types.KICKED,
|
||||
gameId,
|
||||
}),
|
||||
|
||||
cardMoved: (gameId: number, playerId: number, data: MoveCardData) => ({
|
||||
type: Types.CARD_MOVED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardFlipped: (gameId: number, playerId: number, data: FlipCardData) => ({
|
||||
type: Types.CARD_FLIPPED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardDestroyed: (gameId: number, playerId: number, data: DestroyCardData) => ({
|
||||
type: Types.CARD_DESTROYED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardAttached: (gameId: number, playerId: number, data: AttachCardData) => ({
|
||||
type: Types.CARD_ATTACHED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
tokenCreated: (gameId: number, playerId: number, data: CreateTokenData) => ({
|
||||
type: Types.TOKEN_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardAttrChanged: (gameId: number, playerId: number, data: SetCardAttrData) => ({
|
||||
type: Types.CARD_ATTR_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardCounterChanged: (gameId: number, playerId: number, data: SetCardCounterData) => ({
|
||||
type: Types.CARD_COUNTER_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
arrowCreated: (gameId: number, playerId: number, data: CreateArrowData) => ({
|
||||
type: Types.ARROW_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
arrowDeleted: (gameId: number, playerId: number, data: DeleteArrowData) => ({
|
||||
type: Types.ARROW_DELETED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterCreated: (gameId: number, playerId: number, data: CreateCounterData) => ({
|
||||
type: Types.COUNTER_CREATED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterSet: (gameId: number, playerId: number, data: SetCounterData) => ({
|
||||
type: Types.COUNTER_SET,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
counterDeleted: (gameId: number, playerId: number, data: DelCounterData) => ({
|
||||
type: Types.COUNTER_DELETED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardsDrawn: (gameId: number, playerId: number, data: DrawCardsData) => ({
|
||||
type: Types.CARDS_DRAWN,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
cardsRevealed: (gameId: number, playerId: number, data: RevealCardsData) => ({
|
||||
type: Types.CARDS_REVEALED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
zoneShuffled: (gameId: number, playerId: number, data: ShuffleData) => ({
|
||||
type: Types.ZONE_SHUFFLED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
dieRolled: (gameId: number, playerId: number, data: RollDieData) => ({
|
||||
type: Types.DIE_ROLLED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
activePlayerSet: (gameId: number, activePlayerId: number) => ({
|
||||
type: Types.ACTIVE_PLAYER_SET,
|
||||
gameId,
|
||||
activePlayerId,
|
||||
}),
|
||||
|
||||
activePhaseSet: (gameId: number, phase: number) => ({
|
||||
type: Types.ACTIVE_PHASE_SET,
|
||||
gameId,
|
||||
phase,
|
||||
}),
|
||||
|
||||
turnReversed: (gameId: number, reversed: boolean) => ({
|
||||
type: Types.TURN_REVERSED,
|
||||
gameId,
|
||||
reversed,
|
||||
}),
|
||||
|
||||
zoneDumped: (gameId: number, playerId: number, data: DumpZoneData) => ({
|
||||
type: Types.ZONE_DUMPED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
zonePropertiesChanged: (gameId: number, playerId: number, data: ChangeZonePropertiesData) => ({
|
||||
type: Types.ZONE_PROPERTIES_CHANGED,
|
||||
gameId,
|
||||
playerId,
|
||||
data,
|
||||
}),
|
||||
|
||||
gameSay: (gameId: number, playerId: number, message: string) => ({
|
||||
type: Types.GAME_SAY,
|
||||
gameId,
|
||||
playerId,
|
||||
message,
|
||||
}),
|
||||
};
|
||||
198
webclient/src/store/game/game.dispatch.spec.ts
Normal file
198
webclient/src/store/game/game.dispatch.spec.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
||||
|
||||
import { store } from 'store/store';
|
||||
import { Actions } from './game.actions';
|
||||
import { Dispatch } from './game.dispatch';
|
||||
import {
|
||||
makeArrow,
|
||||
makeCard,
|
||||
makeCounter,
|
||||
makeGameEntry,
|
||||
makePlayerProperties,
|
||||
} from './__mocks__/fixtures';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('Dispatch', () => {
|
||||
it('clearStore dispatches Actions.clearStore()', () => {
|
||||
Dispatch.clearStore();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.clearStore());
|
||||
});
|
||||
|
||||
it('gameJoined dispatches Actions.gameJoined()', () => {
|
||||
const entry = makeGameEntry();
|
||||
Dispatch.gameJoined(1, entry);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameJoined(1, entry));
|
||||
});
|
||||
|
||||
it('gameLeft dispatches Actions.gameLeft()', () => {
|
||||
Dispatch.gameLeft(2);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameLeft(2));
|
||||
});
|
||||
|
||||
it('gameClosed dispatches Actions.gameClosed()', () => {
|
||||
Dispatch.gameClosed(3);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameClosed(3));
|
||||
});
|
||||
|
||||
it('gameHostChanged dispatches Actions.gameHostChanged()', () => {
|
||||
Dispatch.gameHostChanged(1, 7);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameHostChanged(1, 7));
|
||||
});
|
||||
|
||||
it('gameStateChanged dispatches Actions.gameStateChanged()', () => {
|
||||
const data = { playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0 };
|
||||
Dispatch.gameStateChanged(1, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameStateChanged(1, data));
|
||||
});
|
||||
|
||||
it('playerJoined dispatches Actions.playerJoined()', () => {
|
||||
const props = makePlayerProperties();
|
||||
Dispatch.playerJoined(1, props);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerJoined(1, props));
|
||||
});
|
||||
|
||||
it('playerLeft dispatches Actions.playerLeft()', () => {
|
||||
Dispatch.playerLeft(1, 2, 3);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerLeft(1, 2, 3));
|
||||
});
|
||||
|
||||
it('playerPropertiesChanged dispatches Actions.playerPropertiesChanged()', () => {
|
||||
const props = makePlayerProperties();
|
||||
Dispatch.playerPropertiesChanged(1, 2, props);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.playerPropertiesChanged(1, 2, props));
|
||||
});
|
||||
|
||||
it('kicked dispatches Actions.kicked()', () => {
|
||||
Dispatch.kicked(1);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.kicked(1));
|
||||
});
|
||||
|
||||
it('cardMoved dispatches Actions.cardMoved()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardMoved(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardFlipped dispatches Actions.cardFlipped()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardFlipped(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardDestroyed dispatches Actions.cardDestroyed()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardDestroyed(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardAttached dispatches Actions.cardAttached()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardAttached(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data));
|
||||
});
|
||||
|
||||
it('tokenCreated dispatches Actions.tokenCreated()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.tokenCreated(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardAttrChanged(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => {
|
||||
const data = { cardId: 1 } as any;
|
||||
Dispatch.cardCounterChanged(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data));
|
||||
});
|
||||
|
||||
it('arrowCreated dispatches Actions.arrowCreated()', () => {
|
||||
const data = { arrowInfo: makeArrow() };
|
||||
Dispatch.arrowCreated(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data));
|
||||
});
|
||||
|
||||
it('arrowDeleted dispatches Actions.arrowDeleted()', () => {
|
||||
const data = { arrowId: 3 };
|
||||
Dispatch.arrowDeleted(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data));
|
||||
});
|
||||
|
||||
it('counterCreated dispatches Actions.counterCreated()', () => {
|
||||
const data = { counterInfo: makeCounter() };
|
||||
Dispatch.counterCreated(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data));
|
||||
});
|
||||
|
||||
it('counterSet dispatches Actions.counterSet()', () => {
|
||||
const data = { counterId: 1, value: 10 };
|
||||
Dispatch.counterSet(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data));
|
||||
});
|
||||
|
||||
it('counterDeleted dispatches Actions.counterDeleted()', () => {
|
||||
const data = { counterId: 1 };
|
||||
Dispatch.counterDeleted(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardsDrawn dispatches Actions.cardsDrawn()', () => {
|
||||
const data = { number: 2, cards: [makeCard()] };
|
||||
Dispatch.cardsDrawn(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data));
|
||||
});
|
||||
|
||||
it('cardsRevealed dispatches Actions.cardsRevealed()', () => {
|
||||
const data = { zoneName: 'hand', cards: [] } as any;
|
||||
Dispatch.cardsRevealed(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data));
|
||||
});
|
||||
|
||||
it('zoneShuffled dispatches Actions.zoneShuffled()', () => {
|
||||
const data = { zoneName: 'deck', start: 0, end: 39 };
|
||||
Dispatch.zoneShuffled(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data));
|
||||
});
|
||||
|
||||
it('dieRolled dispatches Actions.dieRolled()', () => {
|
||||
const data = { sides: 6, value: 4, values: [4] };
|
||||
Dispatch.dieRolled(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data));
|
||||
});
|
||||
|
||||
it('activePlayerSet dispatches Actions.activePlayerSet()', () => {
|
||||
Dispatch.activePlayerSet(1, 3);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.activePlayerSet(1, 3));
|
||||
});
|
||||
|
||||
it('activePhaseSet dispatches Actions.activePhaseSet()', () => {
|
||||
Dispatch.activePhaseSet(1, 2);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.activePhaseSet(1, 2));
|
||||
});
|
||||
|
||||
it('turnReversed dispatches Actions.turnReversed()', () => {
|
||||
Dispatch.turnReversed(1, true);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.turnReversed(1, true));
|
||||
});
|
||||
|
||||
it('zoneDumped dispatches Actions.zoneDumped()', () => {
|
||||
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
|
||||
Dispatch.zoneDumped(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data));
|
||||
});
|
||||
|
||||
it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => {
|
||||
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
|
||||
Dispatch.zonePropertiesChanged(1, 2, data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data));
|
||||
});
|
||||
|
||||
it('gameSay dispatches Actions.gameSay()', () => {
|
||||
Dispatch.gameSay(1, 2, 'gg wp');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameSay(1, 2, 'gg wp'));
|
||||
});
|
||||
});
|
||||
155
webclient/src/store/game/game.dispatch.ts
Normal file
155
webclient/src/store/game/game.dispatch.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
AttachCardData,
|
||||
ChangeZonePropertiesData,
|
||||
CreateArrowData,
|
||||
CreateCounterData,
|
||||
CreateTokenData,
|
||||
DelCounterData,
|
||||
DeleteArrowData,
|
||||
DestroyCardData,
|
||||
DrawCardsData,
|
||||
DumpZoneData,
|
||||
FlipCardData,
|
||||
GameStateChangedData,
|
||||
MoveCardData,
|
||||
PlayerProperties,
|
||||
RevealCardsData,
|
||||
RollDieData,
|
||||
SetCardAttrData,
|
||||
SetCardCounterData,
|
||||
SetCounterData,
|
||||
ShuffleData,
|
||||
} from 'types';
|
||||
import { store } from 'store/store';
|
||||
import { Actions } from './game.actions';
|
||||
import { GameEntry } from './game.interfaces';
|
||||
|
||||
export const Dispatch = {
|
||||
clearStore: () => {
|
||||
store.dispatch(Actions.clearStore());
|
||||
},
|
||||
|
||||
gameJoined: (gameId: number, gameEntry: GameEntry) => {
|
||||
store.dispatch(Actions.gameJoined(gameId, gameEntry));
|
||||
},
|
||||
|
||||
gameLeft: (gameId: number) => {
|
||||
store.dispatch(Actions.gameLeft(gameId));
|
||||
},
|
||||
|
||||
gameClosed: (gameId: number) => {
|
||||
store.dispatch(Actions.gameClosed(gameId));
|
||||
},
|
||||
|
||||
gameHostChanged: (gameId: number, hostId: number) => {
|
||||
store.dispatch(Actions.gameHostChanged(gameId, hostId));
|
||||
},
|
||||
|
||||
gameStateChanged: (gameId: number, data: GameStateChangedData) => {
|
||||
store.dispatch(Actions.gameStateChanged(gameId, data));
|
||||
},
|
||||
|
||||
playerJoined: (gameId: number, playerProperties: PlayerProperties) => {
|
||||
store.dispatch(Actions.playerJoined(gameId, playerProperties));
|
||||
},
|
||||
|
||||
playerLeft: (gameId: number, playerId: number, reason: number) => {
|
||||
store.dispatch(Actions.playerLeft(gameId, playerId, reason));
|
||||
},
|
||||
|
||||
playerPropertiesChanged: (gameId: number, playerId: number, properties: PlayerProperties) => {
|
||||
store.dispatch(Actions.playerPropertiesChanged(gameId, playerId, properties));
|
||||
},
|
||||
|
||||
kicked: (gameId: number) => {
|
||||
store.dispatch(Actions.kicked(gameId));
|
||||
},
|
||||
|
||||
cardMoved: (gameId: number, playerId: number, data: MoveCardData) => {
|
||||
store.dispatch(Actions.cardMoved(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardFlipped: (gameId: number, playerId: number, data: FlipCardData) => {
|
||||
store.dispatch(Actions.cardFlipped(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardDestroyed: (gameId: number, playerId: number, data: DestroyCardData) => {
|
||||
store.dispatch(Actions.cardDestroyed(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardAttached: (gameId: number, playerId: number, data: AttachCardData) => {
|
||||
store.dispatch(Actions.cardAttached(gameId, playerId, data));
|
||||
},
|
||||
|
||||
tokenCreated: (gameId: number, playerId: number, data: CreateTokenData) => {
|
||||
store.dispatch(Actions.tokenCreated(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardAttrChanged: (gameId: number, playerId: number, data: SetCardAttrData) => {
|
||||
store.dispatch(Actions.cardAttrChanged(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardCounterChanged: (gameId: number, playerId: number, data: SetCardCounterData) => {
|
||||
store.dispatch(Actions.cardCounterChanged(gameId, playerId, data));
|
||||
},
|
||||
|
||||
arrowCreated: (gameId: number, playerId: number, data: CreateArrowData) => {
|
||||
store.dispatch(Actions.arrowCreated(gameId, playerId, data));
|
||||
},
|
||||
|
||||
arrowDeleted: (gameId: number, playerId: number, data: DeleteArrowData) => {
|
||||
store.dispatch(Actions.arrowDeleted(gameId, playerId, data));
|
||||
},
|
||||
|
||||
counterCreated: (gameId: number, playerId: number, data: CreateCounterData) => {
|
||||
store.dispatch(Actions.counterCreated(gameId, playerId, data));
|
||||
},
|
||||
|
||||
counterSet: (gameId: number, playerId: number, data: SetCounterData) => {
|
||||
store.dispatch(Actions.counterSet(gameId, playerId, data));
|
||||
},
|
||||
|
||||
counterDeleted: (gameId: number, playerId: number, data: DelCounterData) => {
|
||||
store.dispatch(Actions.counterDeleted(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardsDrawn: (gameId: number, playerId: number, data: DrawCardsData) => {
|
||||
store.dispatch(Actions.cardsDrawn(gameId, playerId, data));
|
||||
},
|
||||
|
||||
cardsRevealed: (gameId: number, playerId: number, data: RevealCardsData) => {
|
||||
store.dispatch(Actions.cardsRevealed(gameId, playerId, data));
|
||||
},
|
||||
|
||||
zoneShuffled: (gameId: number, playerId: number, data: ShuffleData) => {
|
||||
store.dispatch(Actions.zoneShuffled(gameId, playerId, data));
|
||||
},
|
||||
|
||||
dieRolled: (gameId: number, playerId: number, data: RollDieData) => {
|
||||
store.dispatch(Actions.dieRolled(gameId, playerId, data));
|
||||
},
|
||||
|
||||
activePlayerSet: (gameId: number, activePlayerId: number) => {
|
||||
store.dispatch(Actions.activePlayerSet(gameId, activePlayerId));
|
||||
},
|
||||
|
||||
activePhaseSet: (gameId: number, phase: number) => {
|
||||
store.dispatch(Actions.activePhaseSet(gameId, phase));
|
||||
},
|
||||
|
||||
turnReversed: (gameId: number, reversed: boolean) => {
|
||||
store.dispatch(Actions.turnReversed(gameId, reversed));
|
||||
},
|
||||
|
||||
zoneDumped: (gameId: number, playerId: number, data: DumpZoneData) => {
|
||||
store.dispatch(Actions.zoneDumped(gameId, playerId, data));
|
||||
},
|
||||
|
||||
zonePropertiesChanged: (gameId: number, playerId: number, data: ChangeZonePropertiesData) => {
|
||||
store.dispatch(Actions.zonePropertiesChanged(gameId, playerId, data));
|
||||
},
|
||||
|
||||
gameSay: (gameId: number, playerId: number, message: string) => {
|
||||
store.dispatch(Actions.gameSay(gameId, playerId, message));
|
||||
},
|
||||
};
|
||||
60
webclient/src/store/game/game.interfaces.ts
Normal file
60
webclient/src/store/game/game.interfaces.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { ArrowInfo, CardInfo, CounterInfo, PlayerProperties } from 'types';
|
||||
|
||||
export interface GamesState {
|
||||
games: { [gameId: number]: GameEntry };
|
||||
}
|
||||
|
||||
/**
|
||||
* Full runtime state for a single active game (played or spectated).
|
||||
* Keyed by gameId in GamesState so multiple concurrent games are supported.
|
||||
*/
|
||||
export interface GameEntry {
|
||||
gameId: number;
|
||||
roomId: number;
|
||||
description: string;
|
||||
hostId: number;
|
||||
/** The playerId assigned to the local user in this game. */
|
||||
localPlayerId: number;
|
||||
spectator: boolean;
|
||||
judge: boolean;
|
||||
resuming: boolean;
|
||||
started: boolean;
|
||||
activePlayerId: number;
|
||||
activePhase: number;
|
||||
secondsElapsed: number;
|
||||
reversed: boolean;
|
||||
players: { [playerId: number]: PlayerEntry };
|
||||
messages: GameMessage[];
|
||||
}
|
||||
|
||||
/** Normalized from ServerInfo_Player — keyed collections for O(1) lookup. */
|
||||
export interface PlayerEntry {
|
||||
properties: PlayerProperties;
|
||||
deckList: string;
|
||||
/** Zones keyed by zone name (e.g. "hand", "deck", "table"). */
|
||||
zones: { [zoneName: string]: ZoneEntry };
|
||||
/** Player-level counters (e.g. life) keyed by counter id. */
|
||||
counters: { [counterId: number]: CounterInfo };
|
||||
/** Arrows keyed by arrow id. */
|
||||
arrows: { [arrowId: number]: ArrowInfo };
|
||||
}
|
||||
|
||||
/** Normalized from ServerInfo_Zone — card list is an ordered array matching proto. */
|
||||
export interface ZoneEntry {
|
||||
name: string;
|
||||
/** ZoneType enum value (0=Private, 1=Public, 2=Hidden). */
|
||||
type: number;
|
||||
withCoords: boolean;
|
||||
/** Authoritative card count (used for hidden zones where cardList may be empty). */
|
||||
cardCount: number;
|
||||
/** Ordered card list; may be empty for hidden zones with no dump active. */
|
||||
cards: CardInfo[];
|
||||
alwaysRevealTopCard: boolean;
|
||||
alwaysLookAtTopCard: boolean;
|
||||
}
|
||||
|
||||
export interface GameMessage {
|
||||
playerId: number;
|
||||
message: string;
|
||||
timeReceived: number;
|
||||
}
|
||||
1341
webclient/src/store/game/game.reducer.spec.ts
Normal file
1341
webclient/src/store/game/game.reducer.spec.ts
Normal file
File diff suppressed because it is too large
Load diff
729
webclient/src/store/game/game.reducer.ts
Normal file
729
webclient/src/store/game/game.reducer.ts
Normal file
|
|
@ -0,0 +1,729 @@
|
|||
import {
|
||||
ArrowInfo,
|
||||
CardAttribute,
|
||||
CardCounterInfo,
|
||||
CardInfo,
|
||||
CounterInfo,
|
||||
PlayerInfo,
|
||||
PlayerProperties,
|
||||
} from 'types';
|
||||
import { GameEntry, GameMessage, GamesState, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||
import { Types } from './game.types';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function updateGame(state: GamesState, gameId: number, updates: Partial<GameEntry>): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
games: { ...state.games, [gameId]: { ...game, ...updates } },
|
||||
};
|
||||
}
|
||||
|
||||
function updatePlayer(
|
||||
state: GamesState,
|
||||
gameId: number,
|
||||
playerId: number,
|
||||
updates: Partial<PlayerEntry>
|
||||
): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updateGame(state, gameId, {
|
||||
players: { ...game.players, [playerId]: { ...player, ...updates } },
|
||||
});
|
||||
}
|
||||
|
||||
function updateZone(
|
||||
state: GamesState,
|
||||
gameId: number,
|
||||
playerId: number,
|
||||
zoneName: string,
|
||||
updates: Partial<ZoneEntry>
|
||||
): GamesState {
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
zones: { ...player.zones, [zoneName]: { ...zone, ...updates } },
|
||||
});
|
||||
}
|
||||
|
||||
function removeGame(state: GamesState, gameId: number): GamesState {
|
||||
const games = { ...state.games };
|
||||
delete games[gameId];
|
||||
return { ...state, games };
|
||||
}
|
||||
|
||||
/** Converts the proto PlayerInfo[] array into the keyed PlayerEntry map used in the store. */
|
||||
function normalizePlayers(playerList: PlayerInfo[]): { [playerId: number]: PlayerEntry } {
|
||||
const players: { [playerId: number]: PlayerEntry } = {};
|
||||
for (const player of playerList) {
|
||||
const playerId = player.properties.playerId;
|
||||
|
||||
const zones: { [zoneName: string]: ZoneEntry } = {};
|
||||
for (const zone of player.zoneList) {
|
||||
zones[zone.name] = {
|
||||
name: zone.name,
|
||||
type: zone.type,
|
||||
withCoords: zone.withCoords,
|
||||
cardCount: zone.cardCount,
|
||||
cards: [...zone.cardList],
|
||||
alwaysRevealTopCard: zone.alwaysRevealTopCard,
|
||||
alwaysLookAtTopCard: zone.alwaysLookAtTopCard,
|
||||
};
|
||||
}
|
||||
|
||||
const counters: { [counterId: number]: CounterInfo } = {};
|
||||
for (const counter of player.counterList) {
|
||||
counters[counter.id] = counter;
|
||||
}
|
||||
|
||||
const arrows: { [arrowId: number]: ArrowInfo } = {};
|
||||
for (const arrow of player.arrowList) {
|
||||
arrows[arrow.id] = arrow;
|
||||
}
|
||||
|
||||
players[playerId] = {
|
||||
properties: player.properties,
|
||||
deckList: player.deckList,
|
||||
zones,
|
||||
counters,
|
||||
arrows,
|
||||
};
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
function buildEmptyCard(
|
||||
id: number,
|
||||
name: string,
|
||||
x: number,
|
||||
y: number,
|
||||
faceDown: boolean,
|
||||
providerId: string
|
||||
): CardInfo {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
tapped: false,
|
||||
attacking: false,
|
||||
color: '',
|
||||
pt: '',
|
||||
annotation: '',
|
||||
destroyOnZoneChange: false,
|
||||
doesntUntap: false,
|
||||
counterList: [],
|
||||
attachPlayerId: -1,
|
||||
attachZone: '',
|
||||
attachCardId: -1,
|
||||
providerId,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Initial state ─────────────────────────────────────────────────────────────
|
||||
|
||||
const initialState: GamesState = {
|
||||
games: {},
|
||||
};
|
||||
|
||||
// ── Reducer ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const gamesReducer = (state: GamesState = initialState, action: any): GamesState => {
|
||||
switch (action.type) {
|
||||
case Types.CLEAR_STORE: {
|
||||
return initialState;
|
||||
}
|
||||
|
||||
case Types.GAME_JOINED: {
|
||||
return {
|
||||
...state,
|
||||
games: { ...state.games, [action.gameId]: action.gameEntry },
|
||||
};
|
||||
}
|
||||
|
||||
case Types.GAME_LEFT:
|
||||
case Types.GAME_CLOSED:
|
||||
case Types.KICKED: {
|
||||
return removeGame(state, action.gameId);
|
||||
}
|
||||
|
||||
case Types.GAME_HOST_CHANGED: {
|
||||
return updateGame(state, action.gameId, { hostId: action.hostId });
|
||||
}
|
||||
|
||||
case Types.GAME_STATE_CHANGED: {
|
||||
const { gameId, data } = action;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updates: Partial<GameEntry> = {};
|
||||
if (data.playerList?.length > 0) {
|
||||
updates.players = normalizePlayers(data.playerList);
|
||||
}
|
||||
if (data.gameStarted !== undefined && data.gameStarted !== null) {
|
||||
updates.started = data.gameStarted;
|
||||
}
|
||||
if (data.activePlayerId !== undefined && data.activePlayerId !== null) {
|
||||
updates.activePlayerId = data.activePlayerId;
|
||||
}
|
||||
if (data.activePhase !== undefined && data.activePhase !== null) {
|
||||
updates.activePhase = data.activePhase;
|
||||
}
|
||||
if (data.secondsElapsed !== undefined) {
|
||||
updates.secondsElapsed = data.secondsElapsed;
|
||||
}
|
||||
return updateGame(state, gameId, updates);
|
||||
}
|
||||
|
||||
case Types.PLAYER_JOINED: {
|
||||
const { gameId, playerProperties } = action;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const newPlayer: PlayerEntry = {
|
||||
properties: playerProperties as PlayerProperties,
|
||||
deckList: '',
|
||||
zones: {},
|
||||
counters: {},
|
||||
arrows: {},
|
||||
};
|
||||
return updateGame(state, gameId, {
|
||||
players: { ...game.players, [playerProperties.playerId]: newPlayer },
|
||||
});
|
||||
}
|
||||
|
||||
case Types.PLAYER_LEFT: {
|
||||
const { gameId, playerId } = action;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const players = { ...game.players };
|
||||
delete players[playerId];
|
||||
return updateGame(state, gameId, { players });
|
||||
}
|
||||
|
||||
case Types.PLAYER_PROPERTIES_CHANGED: {
|
||||
return updatePlayer(state, action.gameId, action.playerId, {
|
||||
properties: action.properties,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Card manipulation ────────────────────────────────────────────────────
|
||||
|
||||
case Types.CARD_MOVED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const {
|
||||
cardId,
|
||||
cardName,
|
||||
startPlayerId,
|
||||
startZone,
|
||||
position,
|
||||
targetPlayerId,
|
||||
targetZone,
|
||||
x,
|
||||
y,
|
||||
newCardId,
|
||||
faceDown,
|
||||
newCardProviderId,
|
||||
} = data;
|
||||
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const effectiveStartPlayerId = startPlayerId >= 0 ? startPlayerId : playerId;
|
||||
const sourcePlayer = game.players[effectiveStartPlayerId];
|
||||
const sourceZoneEntry = sourcePlayer?.zones[startZone];
|
||||
if (!sourcePlayer || !sourceZoneEntry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Locate card in source zone (by id for visible zones, by position for hidden)
|
||||
let removedCard: CardInfo | undefined;
|
||||
let newSourceCards: CardInfo[];
|
||||
if (cardId >= 0) {
|
||||
removedCard = sourceZoneEntry.cards.find(c => c.id === cardId);
|
||||
newSourceCards = sourceZoneEntry.cards.filter(c => c.id !== cardId);
|
||||
} else if (position >= 0 && position < sourceZoneEntry.cards.length) {
|
||||
removedCard = sourceZoneEntry.cards[position];
|
||||
newSourceCards = sourceZoneEntry.cards.filter((_, i) => i !== position);
|
||||
} else {
|
||||
// Hidden zone with unknown position — just decrement count
|
||||
newSourceCards = sourceZoneEntry.cards;
|
||||
}
|
||||
|
||||
const effectiveNewId = newCardId >= 0 ? newCardId : (removedCard?.id ?? -1);
|
||||
const movedCard: CardInfo = removedCard
|
||||
? {
|
||||
...removedCard,
|
||||
id: effectiveNewId,
|
||||
name: cardName || removedCard.name,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
providerId: newCardProviderId || removedCard.providerId,
|
||||
}
|
||||
: buildEmptyCard(effectiveNewId, cardName, x, y, faceDown, newCardProviderId ?? '');
|
||||
|
||||
let newState = updateZone(state, gameId, effectiveStartPlayerId, startZone, {
|
||||
cards: newSourceCards,
|
||||
cardCount: Math.max(0, sourceZoneEntry.cardCount - 1),
|
||||
});
|
||||
|
||||
const updatedGame = newState.games[gameId];
|
||||
const targetPlayer = updatedGame?.players[targetPlayerId];
|
||||
const targetZoneEntry = targetPlayer?.zones[targetZone];
|
||||
if (!targetPlayer || !targetZoneEntry) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
newState = updateZone(newState, gameId, targetPlayerId, targetZone, {
|
||||
cards: [...targetZoneEntry.cards, movedCard],
|
||||
cardCount: targetZoneEntry.cardCount + 1,
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case Types.CARD_FLIPPED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, cardId, cardName, faceDown, cardProviderId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = {
|
||||
...updatedCards[cardIdx],
|
||||
faceDown,
|
||||
name: cardName || updatedCards[cardIdx].name,
|
||||
providerId: cardProviderId || updatedCards[cardIdx].providerId,
|
||||
};
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.CARD_DESTROYED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, cardId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return updateZone(state, gameId, playerId, zoneName, {
|
||||
cards: zone.cards.filter(c => c.id !== cardId),
|
||||
cardCount: Math.max(0, zone.cardCount - 1),
|
||||
});
|
||||
}
|
||||
|
||||
case Types.CARD_ATTACHED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { startZone, cardId, targetPlayerId, targetZone, targetCardId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[startZone];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = {
|
||||
...updatedCards[cardIdx],
|
||||
attachPlayerId: targetPlayerId,
|
||||
attachZone: targetZone,
|
||||
attachCardId: targetCardId,
|
||||
};
|
||||
return updateZone(state, gameId, playerId, startZone, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.TOKEN_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const {
|
||||
zoneName,
|
||||
cardId,
|
||||
cardName,
|
||||
color,
|
||||
pt,
|
||||
annotation,
|
||||
destroyOnZoneChange,
|
||||
x,
|
||||
y,
|
||||
cardProviderId,
|
||||
faceDown,
|
||||
} = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newCard: CardInfo = {
|
||||
id: cardId,
|
||||
name: cardName,
|
||||
x,
|
||||
y,
|
||||
faceDown,
|
||||
tapped: false,
|
||||
attacking: false,
|
||||
color,
|
||||
pt,
|
||||
annotation,
|
||||
destroyOnZoneChange,
|
||||
doesntUntap: false,
|
||||
counterList: [],
|
||||
attachPlayerId: -1,
|
||||
attachZone: '',
|
||||
attachCardId: -1,
|
||||
providerId: cardProviderId,
|
||||
};
|
||||
return updateZone(state, gameId, playerId, zoneName, {
|
||||
cards: [...zone.cards, newCard],
|
||||
cardCount: zone.cardCount + 1,
|
||||
});
|
||||
}
|
||||
|
||||
case Types.CARD_ATTR_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, cardId, attribute, attrValue } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const attrPatch: Partial<CardInfo> = {};
|
||||
switch (attribute as CardAttribute) {
|
||||
case CardAttribute.AttrTapped: attrPatch.tapped = attrValue === '1'; break;
|
||||
case CardAttribute.AttrAttacking: attrPatch.attacking = attrValue === '1'; break;
|
||||
case CardAttribute.AttrFaceDown: attrPatch.faceDown = attrValue === '1'; break;
|
||||
case CardAttribute.AttrColor: attrPatch.color = attrValue; break;
|
||||
case CardAttribute.AttrPT: attrPatch.pt = attrValue; break;
|
||||
case CardAttribute.AttrAnnotation: attrPatch.annotation = attrValue; break;
|
||||
case CardAttribute.AttrDoesntUntap: attrPatch.doesntUntap = attrValue === '1'; break;
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = { ...updatedCards[cardIdx], ...attrPatch };
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
|
||||
case Types.CARD_COUNTER_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, cardId, counterId, counterValue } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const cardIdx = zone.cards.findIndex(c => c.id === cardId);
|
||||
if (cardIdx < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const card = zone.cards[cardIdx];
|
||||
let newCounterList: CardCounterInfo[];
|
||||
if (counterValue <= 0) {
|
||||
newCounterList = card.counterList.filter(c => c.id !== counterId);
|
||||
} else {
|
||||
const existing = card.counterList.findIndex(c => c.id === counterId);
|
||||
newCounterList =
|
||||
existing >= 0
|
||||
? card.counterList.map(c => (c.id === counterId ? { ...c, value: counterValue } : c))
|
||||
: [...card.counterList, { id: counterId, value: counterValue }];
|
||||
}
|
||||
|
||||
const updatedCards = [...zone.cards];
|
||||
updatedCards[cardIdx] = { ...card, counterList: newCounterList };
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: updatedCards });
|
||||
}
|
||||
|
||||
// ── Arrows ───────────────────────────────────────────────────────────────
|
||||
|
||||
case Types.ARROW_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { arrowInfo } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
arrows: { ...player.arrows, [arrowInfo.id]: arrowInfo },
|
||||
});
|
||||
}
|
||||
|
||||
case Types.ARROW_DELETED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { arrowId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const arrows = { ...player.arrows };
|
||||
delete arrows[arrowId];
|
||||
return updatePlayer(state, gameId, playerId, { arrows });
|
||||
}
|
||||
|
||||
// ── Player counters ───────────────────────────────────────────────────────
|
||||
|
||||
case Types.COUNTER_CREATED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterInfo } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
counters: { ...player.counters, [counterInfo.id]: counterInfo },
|
||||
});
|
||||
}
|
||||
|
||||
case Types.COUNTER_SET: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterId, value } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const counter = player.counters[counterId];
|
||||
if (!counter) {
|
||||
return state;
|
||||
}
|
||||
return updatePlayer(state, gameId, playerId, {
|
||||
counters: { ...player.counters, [counterId]: { ...counter, count: value } },
|
||||
});
|
||||
}
|
||||
|
||||
case Types.COUNTER_DELETED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { counterId } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const counters = { ...player.counters };
|
||||
delete counters[counterId];
|
||||
return updatePlayer(state, gameId, playerId, { counters });
|
||||
}
|
||||
|
||||
// ── Zone operations ───────────────────────────────────────────────────────
|
||||
|
||||
case Types.CARDS_DRAWN: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { number: drawCount, cards } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const deckZone = player.zones['deck'];
|
||||
const handZone = player.zones['hand'];
|
||||
if (!handZone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Decrement deck count for the drawing player
|
||||
let newState = deckZone
|
||||
? updateZone(state, gameId, playerId, 'deck', {
|
||||
cardCount: Math.max(0, deckZone.cardCount - drawCount),
|
||||
})
|
||||
: state;
|
||||
|
||||
// Append revealed cards to hand (cards array is empty for non-drawing players;
|
||||
// use drawCount for count math so all observers track the correct hand/deck size)
|
||||
const updatedHand = newState.games[gameId]!.players[playerId]!.zones['hand']!;
|
||||
return updateZone(newState, gameId, playerId, 'hand', {
|
||||
cards: [...updatedHand.cards, ...cards],
|
||||
cardCount: updatedHand.cardCount + drawCount,
|
||||
});
|
||||
}
|
||||
|
||||
case Types.CARDS_REVEALED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, cards } = data;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const player = game.players[playerId];
|
||||
if (!player) {
|
||||
return state;
|
||||
}
|
||||
const zone = player.zones[zoneName];
|
||||
if (!zone) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Merge revealed card data into existing zone cards (update existing, append new)
|
||||
const merged = [...zone.cards];
|
||||
for (const revealedCard of cards) {
|
||||
const idx = merged.findIndex(c => c.id === revealedCard.id);
|
||||
if (idx >= 0) {
|
||||
merged[idx] = { ...merged[idx], ...revealedCard };
|
||||
} else {
|
||||
merged.push(revealedCard);
|
||||
}
|
||||
}
|
||||
return updateZone(state, gameId, playerId, zoneName, { cards: merged });
|
||||
}
|
||||
|
||||
case Types.ZONE_PROPERTIES_CHANGED: {
|
||||
const { gameId, playerId, data } = action;
|
||||
const { zoneName, alwaysRevealTopCard, alwaysLookAtTopCard } = data;
|
||||
const patch: Partial<ZoneEntry> = {};
|
||||
if (alwaysRevealTopCard !== undefined && alwaysRevealTopCard !== null) {
|
||||
patch.alwaysRevealTopCard = alwaysRevealTopCard;
|
||||
}
|
||||
if (alwaysLookAtTopCard !== undefined && alwaysLookAtTopCard !== null) {
|
||||
patch.alwaysLookAtTopCard = alwaysLookAtTopCard;
|
||||
}
|
||||
return updateZone(state, gameId, playerId, zoneName, patch);
|
||||
}
|
||||
|
||||
// ── Turn / phase ──────────────────────────────────────────────────────────
|
||||
|
||||
case Types.ACTIVE_PLAYER_SET: {
|
||||
return updateGame(state, action.gameId, { activePlayerId: action.activePlayerId });
|
||||
}
|
||||
|
||||
case Types.ACTIVE_PHASE_SET: {
|
||||
return updateGame(state, action.gameId, { activePhase: action.phase });
|
||||
}
|
||||
|
||||
case Types.TURN_REVERSED: {
|
||||
return updateGame(state, action.gameId, { reversed: action.reversed });
|
||||
}
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────────────────────────
|
||||
|
||||
case Types.GAME_SAY: {
|
||||
const { gameId, playerId, message } = action;
|
||||
const game = state.games[gameId];
|
||||
if (!game) {
|
||||
return state;
|
||||
}
|
||||
const newMessage: GameMessage = { playerId, message, timeReceived: Date.now() };
|
||||
return updateGame(state, gameId, {
|
||||
messages: [...game.messages, newMessage],
|
||||
});
|
||||
}
|
||||
|
||||
// ── Log-only events (state unchanged, future game log will use these) ─────
|
||||
case Types.ZONE_SHUFFLED:
|
||||
case Types.ZONE_DUMPED:
|
||||
case Types.DIE_ROLLED: {
|
||||
return state;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
158
webclient/src/store/game/game.selectors.spec.ts
Normal file
158
webclient/src/store/game/game.selectors.spec.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { Selectors } from './game.selectors';
|
||||
import {
|
||||
makeGameEntry, makePlayerEntry, makePlayerProperties, makeState,
|
||||
makeZoneEntry, makeCard, makeCounter, makeArrow,
|
||||
} from './__mocks__/fixtures';
|
||||
import { GamesState } from './game.interfaces';
|
||||
|
||||
function rootState(games: GamesState) {
|
||||
return { games };
|
||||
}
|
||||
|
||||
describe('Selectors', () => {
|
||||
it('getGames → returns the games map', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getGames(rootState(state))).toBe(state.games);
|
||||
});
|
||||
|
||||
it('getGame → returns the game entry for a given gameId', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getGame(rootState(state), 1)).toBe(state.games[1]);
|
||||
});
|
||||
|
||||
it('getGame → returns undefined for unknown gameId', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getGame(rootState(state), 999)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getPlayers → returns players map for a game', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getPlayers(rootState(state), 1)).toBe(state.games[1].players);
|
||||
});
|
||||
|
||||
it('getPlayers → returns undefined for unknown gameId', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getPlayers(rootState(state), 999)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getPlayer → returns a specific player', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getPlayer(rootState(state), 1, 1)).toBe(state.games[1].players[1]);
|
||||
});
|
||||
|
||||
it('getLocalPlayerId → returns localPlayerId from game', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ localPlayerId: 42 }) } });
|
||||
expect(Selectors.getLocalPlayerId(rootState(state), 1)).toBe(42);
|
||||
});
|
||||
|
||||
it('getLocalPlayer → returns the player matching localPlayerId', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ localPlayerId: 1 }) } });
|
||||
const result = Selectors.getLocalPlayer(rootState(state), 1);
|
||||
expect(result).toBe(state.games[1].players[1]);
|
||||
});
|
||||
|
||||
it('getLocalPlayer → returns undefined when game is not found', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getLocalPlayer(rootState(state), 999)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getZones → returns zones map for a player', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getZones(rootState(state), 1, 1)).toBe(state.games[1].players[1].zones);
|
||||
});
|
||||
|
||||
it('getZone → returns a specific zone', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getZone(rootState(state), 1, 1, 'hand')).toBe(state.games[1].players[1].zones['hand']);
|
||||
});
|
||||
|
||||
it('getCards → returns cards array for a zone', () => {
|
||||
const card = makeCard();
|
||||
const state = makeState({
|
||||
games: {
|
||||
1: makeGameEntry({
|
||||
players: {
|
||||
1: makePlayerEntry({
|
||||
zones: { hand: makeZoneEntry({ name: 'hand', cards: [card] }) },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(Selectors.getCards(rootState(state), 1, 1, 'hand')).toEqual([card]);
|
||||
});
|
||||
|
||||
it('getCards → returns [] when zone not found', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getCards(rootState(state), 1, 1, 'nonexistent')).toEqual([]);
|
||||
});
|
||||
|
||||
it('getCounters → returns counters map for a player', () => {
|
||||
const counter = makeCounter({ id: 2 });
|
||||
const state = makeState({
|
||||
games: { 1: makeGameEntry({ players: { 1: makePlayerEntry({ counters: { 2: counter } }) } }) },
|
||||
});
|
||||
expect(Selectors.getCounters(rootState(state), 1, 1)).toEqual({ 2: counter });
|
||||
});
|
||||
|
||||
it('getArrows → returns arrows map for a player', () => {
|
||||
const arrow = makeArrow({ id: 3 });
|
||||
const state = makeState({
|
||||
games: { 1: makeGameEntry({ players: { 1: makePlayerEntry({ arrows: { 3: arrow } }) } }) },
|
||||
});
|
||||
expect(Selectors.getArrows(rootState(state), 1, 1)).toEqual({ 3: arrow });
|
||||
});
|
||||
|
||||
it('getActivePlayerId → returns activePlayerId from game', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ activePlayerId: 7 }) } });
|
||||
expect(Selectors.getActivePlayerId(rootState(state), 1)).toBe(7);
|
||||
});
|
||||
|
||||
it('getActivePhase → returns activePhase from game', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ activePhase: 3 }) } });
|
||||
expect(Selectors.getActivePhase(rootState(state), 1)).toBe(3);
|
||||
});
|
||||
|
||||
it('isStarted → returns true when game is started', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ started: true }) } });
|
||||
expect(Selectors.isStarted(rootState(state), 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('isStarted → returns false when game not found', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.isStarted(rootState(state), 999)).toBe(false);
|
||||
});
|
||||
|
||||
it('isSpectator → returns spectator flag from game', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ spectator: true }) } });
|
||||
expect(Selectors.isSpectator(rootState(state), 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('isReversed → returns reversed flag from game', () => {
|
||||
const state = makeState({ games: { 1: makeGameEntry({ reversed: true }) } });
|
||||
expect(Selectors.isReversed(rootState(state), 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('getMessages → returns messages array from game', () => {
|
||||
const messages = [{ playerId: 1, message: 'hi', timeReceived: 100 }];
|
||||
const state = makeState({ games: { 1: makeGameEntry({ messages }) } });
|
||||
expect(Selectors.getMessages(rootState(state), 1)).toBe(messages);
|
||||
});
|
||||
|
||||
it('getMessages → returns [] when game not found', () => {
|
||||
const state = makeState();
|
||||
expect(Selectors.getMessages(rootState(state), 999)).toEqual([]);
|
||||
});
|
||||
|
||||
it('getActiveGameIds → returns numeric array of gameIds', () => {
|
||||
const state = makeState({
|
||||
games: {
|
||||
1: makeGameEntry({ gameId: 1 }),
|
||||
2: makeGameEntry({ gameId: 2 }),
|
||||
},
|
||||
});
|
||||
const ids = Selectors.getActiveGameIds(rootState(state));
|
||||
expect(ids).toEqual(expect.arrayContaining([1, 2]));
|
||||
expect(ids).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
72
webclient/src/store/game/game.selectors.ts
Normal file
72
webclient/src/store/game/game.selectors.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { GamesState, GameEntry, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||
|
||||
interface State {
|
||||
games: GamesState;
|
||||
}
|
||||
|
||||
export const Selectors = {
|
||||
getGames: ({ games }: State): { [gameId: number]: GameEntry } => games.games,
|
||||
|
||||
getGame: ({ games }: State, gameId: number): GameEntry | undefined => games.games[gameId],
|
||||
|
||||
getPlayers: ({ games }: State, gameId: number): { [playerId: number]: PlayerEntry } | undefined =>
|
||||
games.games[gameId]?.players,
|
||||
|
||||
getPlayer: ({ games }: State, gameId: number, playerId: number): PlayerEntry | undefined =>
|
||||
games.games[gameId]?.players[playerId],
|
||||
|
||||
getLocalPlayerId: ({ games }: State, gameId: number): number | undefined =>
|
||||
games.games[gameId]?.localPlayerId,
|
||||
|
||||
getLocalPlayer: (state: State, gameId: number): PlayerEntry | undefined => {
|
||||
const game = state.games.games[gameId];
|
||||
if (!game) {
|
||||
return undefined;
|
||||
}
|
||||
return game.players[game.localPlayerId];
|
||||
},
|
||||
|
||||
getZones: (
|
||||
{ games }: State,
|
||||
gameId: number,
|
||||
playerId: number
|
||||
): { [zoneName: string]: ZoneEntry } | undefined =>
|
||||
games.games[gameId]?.players[playerId]?.zones,
|
||||
|
||||
getZone: (
|
||||
{ games }: State,
|
||||
gameId: number,
|
||||
playerId: number,
|
||||
zoneName: string
|
||||
): ZoneEntry | undefined => games.games[gameId]?.players[playerId]?.zones[zoneName],
|
||||
|
||||
getCards: ({ games }: State, gameId: number, playerId: number, zoneName: string) =>
|
||||
games.games[gameId]?.players[playerId]?.zones[zoneName]?.cards ?? [],
|
||||
|
||||
getCounters: ({ games }: State, gameId: number, playerId: number) =>
|
||||
games.games[gameId]?.players[playerId]?.counters ?? {},
|
||||
|
||||
getArrows: ({ games }: State, gameId: number, playerId: number) =>
|
||||
games.games[gameId]?.players[playerId]?.arrows ?? {},
|
||||
|
||||
getActivePlayerId: ({ games }: State, gameId: number): number | undefined =>
|
||||
games.games[gameId]?.activePlayerId,
|
||||
|
||||
getActivePhase: ({ games }: State, gameId: number): number | undefined =>
|
||||
games.games[gameId]?.activePhase,
|
||||
|
||||
isStarted: ({ games }: State, gameId: number): boolean =>
|
||||
games.games[gameId]?.started ?? false,
|
||||
|
||||
isSpectator: ({ games }: State, gameId: number): boolean =>
|
||||
games.games[gameId]?.spectator ?? false,
|
||||
|
||||
isReversed: ({ games }: State, gameId: number): boolean =>
|
||||
games.games[gameId]?.reversed ?? false,
|
||||
|
||||
getMessages: ({ games }: State, gameId: number) =>
|
||||
games.games[gameId]?.messages ?? [],
|
||||
|
||||
getActiveGameIds: ({ games }: State): number[] =>
|
||||
Object.keys(games.games).map(Number),
|
||||
};
|
||||
34
webclient/src/store/game/game.types.ts
Normal file
34
webclient/src/store/game/game.types.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
export const Types = {
|
||||
CLEAR_STORE: '[Games] Clear Store',
|
||||
GAME_JOINED: '[Games] Game Joined',
|
||||
GAME_LEFT: '[Games] Game Left',
|
||||
GAME_CLOSED: '[Games] Game Closed',
|
||||
GAME_HOST_CHANGED: '[Games] Game Host Changed',
|
||||
GAME_STATE_CHANGED: '[Games] Game State Changed',
|
||||
PLAYER_JOINED: '[Games] Player Joined',
|
||||
PLAYER_LEFT: '[Games] Player Left',
|
||||
PLAYER_PROPERTIES_CHANGED: '[Games] Player Properties Changed',
|
||||
KICKED: '[Games] Kicked',
|
||||
CARD_MOVED: '[Games] Card Moved',
|
||||
CARD_FLIPPED: '[Games] Card Flipped',
|
||||
CARD_DESTROYED: '[Games] Card Destroyed',
|
||||
CARD_ATTACHED: '[Games] Card Attached',
|
||||
TOKEN_CREATED: '[Games] Token Created',
|
||||
CARD_ATTR_CHANGED: '[Games] Card Attribute Changed',
|
||||
CARD_COUNTER_CHANGED: '[Games] Card Counter Changed',
|
||||
ARROW_CREATED: '[Games] Arrow Created',
|
||||
ARROW_DELETED: '[Games] Arrow Deleted',
|
||||
COUNTER_CREATED: '[Games] Counter Created',
|
||||
COUNTER_SET: '[Games] Counter Set',
|
||||
COUNTER_DELETED: '[Games] Counter Deleted',
|
||||
CARDS_DRAWN: '[Games] Cards Drawn',
|
||||
CARDS_REVEALED: '[Games] Cards Revealed',
|
||||
ZONE_SHUFFLED: '[Games] Zone Shuffled',
|
||||
DIE_ROLLED: '[Games] Die Rolled',
|
||||
ACTIVE_PLAYER_SET: '[Games] Active Player Set',
|
||||
ACTIVE_PHASE_SET: '[Games] Active Phase Set',
|
||||
TURN_REVERSED: '[Games] Turn Reversed',
|
||||
ZONE_DUMPED: '[Games] Zone Dumped',
|
||||
ZONE_PROPERTIES_CHANGED: '[Games] Zone Properties Changed',
|
||||
GAME_SAY: '[Games] Game Say',
|
||||
};
|
||||
6
webclient/src/store/game/index.ts
Normal file
6
webclient/src/store/game/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { Types } from './game.types';
|
||||
export { gamesReducer } from './game.reducer';
|
||||
export { Actions } from './game.actions';
|
||||
export { Dispatch } from './game.dispatch';
|
||||
export { Selectors } from './game.selectors';
|
||||
export * from './game.interfaces';
|
||||
|
|
@ -3,8 +3,15 @@ export { store } from './store';
|
|||
// Common
|
||||
export { SortUtil } from './common';
|
||||
|
||||
// Server
|
||||
// Games
|
||||
export {
|
||||
Types as GameTypes,
|
||||
Selectors as GameSelectors,
|
||||
Dispatch as GameDispatch } from './game';
|
||||
|
||||
export * from 'store/game/game.interfaces';
|
||||
|
||||
// Server
|
||||
export {
|
||||
Types as ServerTypes,
|
||||
Selectors as ServerSelectors,
|
||||
|
|
|
|||
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 @@
|
|||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
||||
vi.mock('redux-form', () => ({
|
||||
reset: vi.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(() => vi.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 vi.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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import { gamesReducer } from './game';
|
||||
import { roomsReducer } from './rooms';
|
||||
import { serverReducer } from './server';
|
||||
import { reducer as formReducer } from 'redux-form'
|
||||
import { actionReducer } from './actions'
|
||||
|
||||
export default combineReducers({
|
||||
games: gamesReducer,
|
||||
rooms: roomsReducer,
|
||||
server: serverReducer,
|
||||
|
||||
|
|
|
|||
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,
|
||||
},
|
||||
messages: {},
|
||||
userInfo: {},
|
||||
notifications: [],
|
||||
serverShutdown: null,
|
||||
banUser: '',
|
||||
banHistory: {},
|
||||
warnHistory: {},
|
||||
warnListOptions: [],
|
||||
warnUser: '',
|
||||
adminNotes: {},
|
||||
replays: [],
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
353
webclient/src/store/server/server.actions.spec.ts
Normal file
353
webclient/src/store/server/server.actions.spec.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
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_SUCCESS });
|
||||
});
|
||||
|
||||
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('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 });
|
||||
});
|
||||
|
||||
it('gamesOfUser', () => {
|
||||
const games = [{ gameId: 1 }] as any;
|
||||
expect(Actions.gamesOfUser('alice', games)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
import { Types } from './server.types';
|
||||
|
||||
export const Actions = {
|
||||
|
|
@ -91,7 +91,7 @@ export const Actions = {
|
|||
type: Types.REGISTRATION_REQUIRES_EMAIL,
|
||||
}),
|
||||
registrationSuccess: () => ({
|
||||
type: Types.REGISTRATION_SUCCES,
|
||||
type: Types.REGISTRATION_SUCCESS,
|
||||
}),
|
||||
registrationFailed: (error) => ({
|
||||
type: Types.REGISTRATION_FAILED,
|
||||
|
|
@ -157,11 +157,6 @@ export const Actions = {
|
|||
type: Types.ACCOUNT_IMAGE_CHANGED,
|
||||
user,
|
||||
}),
|
||||
directMessageSent: (userName, message) => ({
|
||||
type: Types.DIRECT_MESSAGE_SENT,
|
||||
userName,
|
||||
message,
|
||||
}),
|
||||
getUserInfo: (userInfo) => ({
|
||||
type: Types.GET_USER_INFO,
|
||||
userInfo,
|
||||
|
|
@ -239,4 +234,5 @@ export const Actions = {
|
|||
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
||||
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
||||
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
||||
gamesOfUser: (userName: string, games: Game[]) => ({ type: Types.GAMES_OF_USER, userName, games }),
|
||||
}
|
||||
|
|
|
|||
389
webclient/src/store/server/server.dispatch.spec.ts
Normal file
389
webclient/src/store/server/server.dispatch.spec.ts
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
||||
vi.mock('redux-form', () => ({
|
||||
reset: vi.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(() => vi.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 vi.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 vi.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('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));
|
||||
});
|
||||
|
||||
it('gamesOfUser dispatches correctly', () => {
|
||||
const games = [{ gameId: 1 }] as any;
|
||||
Dispatch.gamesOfUser('alice', games);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { reset } from 'redux-form';
|
||||
import { Actions } from './server.actions';
|
||||
import { store } from 'store';
|
||||
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
|
||||
export const Dispatch = {
|
||||
initialized: () => {
|
||||
|
|
@ -141,9 +141,6 @@ export const Dispatch = {
|
|||
accountImageChanged: (user) => {
|
||||
store.dispatch(Actions.accountImageChanged(user));
|
||||
},
|
||||
directMessageSent: (userName, message) => {
|
||||
store.dispatch(Actions.directMessageSent(userName, message));
|
||||
},
|
||||
getUserInfo: (userInfo) => {
|
||||
store.dispatch(Actions.getUserInfo(userInfo));
|
||||
},
|
||||
|
|
@ -216,4 +213,7 @@ export const Dispatch = {
|
|||
deckDelete: (deckId: number) => {
|
||||
store.dispatch(Actions.deckDelete(deckId));
|
||||
},
|
||||
gamesOfUser: (userName: string, games: Game[]) => {
|
||||
store.dispatch(Actions.gamesOfUser(userName, games));
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem
|
||||
WarnHistoryItem, BanHistoryItem, DeckList, Game, LogItem, ReplayMatch, SortBy, User, UserSortField, WarnListItem
|
||||
} from 'types';
|
||||
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||
|
||||
|
|
@ -51,7 +51,6 @@ export interface ServerState {
|
|||
user: User;
|
||||
users: User[];
|
||||
sortUsersBy: ServerStateSortUsersBy;
|
||||
connectOptions: WebSocketConnectOptions;
|
||||
messages: {
|
||||
[userName: string]: UserMessageData[];
|
||||
}
|
||||
|
|
@ -72,6 +71,7 @@ export interface ServerState {
|
|||
adminNotes: { [userName: string]: string };
|
||||
replays: ReplayMatch[];
|
||||
backendDecks: DeckList | null;
|
||||
gamesOfUser: { [userName: string]: Game[] };
|
||||
}
|
||||
|
||||
export interface ServerStateStatus {
|
||||
|
|
|
|||
560
webclient/src/store/server/server.reducer.spec.ts
Normal file
560
webclient/src/store/server/server.reducer.spec.ts
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
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 → returns state unchanged', () => {
|
||||
const options = makeConnectOptions();
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_SUCCESS → returns state unchanged', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_SUCCESS });
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
|
||||
it('ACCOUNT_ACTIVATION_FAILED → returns state unchanged', () => {
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.ACCOUNT_ACTIVATION_FAILED });
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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 → sets both bits, preserves IsUser|IsRegistered', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) | IsJudge(16) = 23
|
||||
expect(result.users[0].userLevel).toBe(23);
|
||||
});
|
||||
|
||||
it('shouldBeMod=true, shouldBeJudge=false → sets IsModerator, clears IsJudge, preserves others', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
|
||||
expect(result.users[0].userLevel).toBe(7);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=true → clears IsModerator, sets IsJudge, preserves others', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
|
||||
// IsUser(1) | IsRegistered(2) | IsJudge(16) = 19
|
||||
expect(result.users[0].userLevel).toBe(19);
|
||||
});
|
||||
|
||||
it('shouldBeMod=false, shouldBeJudge=false → clears both bits, preserves IsUser|IsRegistered', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
||||
// IsUser(1) | IsRegistered(2) = 3
|
||||
expect(result.users[0].userLevel).toBe(3);
|
||||
});
|
||||
|
||||
it('shouldBeMod=true on IsUser|IsRegistered only → produces 7, not 4', () => {
|
||||
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: UserLevelFlag.IsUser | UserLevelFlag.IsRegistered })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
|
||||
expect(result.users[0].userLevel).toBe(7);
|
||||
});
|
||||
|
||||
it('non-matching users are left unchanged', () => {
|
||||
const alice = makeUser({ name: 'Alice', userLevel: 7 });
|
||||
const state = makeServerState({ users: [alice, makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
||||
expect(result.users.find(u => u.name === 'Alice').userLevel).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── GAMES_OF_USER ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('GAMES_OF_USER', () => {
|
||||
it('stores games keyed by userName', () => {
|
||||
const games = [{ gameId: 5, roomId: 1 }] as any;
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games });
|
||||
expect(result.gamesOfUser['alice']).toBe(games);
|
||||
});
|
||||
|
||||
it('overwrites previous games for same user', () => {
|
||||
const old = [{ gameId: 1 }] as any;
|
||||
const fresh = [{ gameId: 2 }] as any;
|
||||
const state = makeServerState({ gamesOfUser: { alice: old } });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh });
|
||||
expect(result.gamesOfUser['alice']).toBe(fresh);
|
||||
});
|
||||
|
||||
it('does not affect other users\' entries', () => {
|
||||
const bobGames = [{ gameId: 3 }] as any;
|
||||
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [] });
|
||||
expect(result.gamesOfUser['bob']).toBe(bobGames);
|
||||
});
|
||||
});
|
||||
|
|
@ -80,7 +80,6 @@ const initialState: ServerState = {
|
|||
field: UserSortField.NAME,
|
||||
order: SortDirection.ASC
|
||||
},
|
||||
connectOptions: {},
|
||||
messages: {},
|
||||
userInfo: {},
|
||||
notifications: [],
|
||||
|
|
@ -93,6 +92,7 @@ const initialState: ServerState = {
|
|||
adminNotes: {},
|
||||
replays: [],
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
};
|
||||
|
||||
export const serverReducer = (state = initialState, action: any) => {
|
||||
|
|
@ -104,19 +104,11 @@ export const serverReducer = (state = initialState, action: any) => {
|
|||
}
|
||||
}
|
||||
case Types.ACCOUNT_AWAITING_ACTIVATION: {
|
||||
return {
|
||||
...state,
|
||||
connectOptions: {
|
||||
...action.options
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case Types.ACCOUNT_ACTIVATION_FAILED:
|
||||
case Types.ACCOUNT_ACTIVATION_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
connectOptions: {}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case Types.CLEAR_STORE: {
|
||||
return {
|
||||
|
|
@ -401,11 +393,12 @@ export const serverReducer = (state = initialState, action: any) => {
|
|||
if (user.name !== userName) {
|
||||
return user;
|
||||
}
|
||||
const judgeFlag = shouldBeJudge ? UserLevelFlag.IsJudge : UserLevelFlag.IsNothing;
|
||||
const modFlag = shouldBeMod ? UserLevelFlag.IsModerator : UserLevelFlag.IsNothing;
|
||||
let newLevel = user.userLevel;
|
||||
newLevel = shouldBeMod ? (newLevel | UserLevelFlag.IsModerator) : (newLevel & ~UserLevelFlag.IsModerator);
|
||||
newLevel = shouldBeJudge ? (newLevel | UserLevelFlag.IsJudge) : (newLevel & ~UserLevelFlag.IsJudge);
|
||||
return {
|
||||
...user,
|
||||
userLevel: user.userLevel & (judgeFlag | modFlag)
|
||||
userLevel: newLevel,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
|
@ -475,6 +468,16 @@ export const serverReducer = (state = initialState, action: any) => {
|
|||
},
|
||||
};
|
||||
}
|
||||
case Types.GAMES_OF_USER: {
|
||||
const { userName, games } = action;
|
||||
return {
|
||||
...state,
|
||||
gamesOfUser: {
|
||||
...state.gamesOfUser,
|
||||
[userName]: games,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
92
webclient/src/store/server/server.selectors.spec.ts
Normal file
92
webclient/src/store/server/server.selectors.spec.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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('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();
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,6 @@ interface State {
|
|||
|
||||
export const Selectors = {
|
||||
getInitialized: ({ server }: State) => server.initialized,
|
||||
getConnectOptions: ({ server }: State) => server.connectOptions,
|
||||
getMessage: ({ server }: State) => server.info.message,
|
||||
getName: ({ server }: State) => server.info.name,
|
||||
getVersion: ({ server }: State) => server.info.version,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const Types = {
|
|||
VIEW_LOGS: '[Server] View Logs',
|
||||
CLEAR_LOGS: '[Server] Clear Logs',
|
||||
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
|
||||
REGISTRATION_SUCCES: '[Server] Registration Success',
|
||||
REGISTRATION_SUCCESS: '[Server] Registration Success',
|
||||
REGISTRATION_FAILED: '[Server] Registration Failed',
|
||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||
|
|
@ -42,7 +42,6 @@ export const Types = {
|
|||
ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change',
|
||||
ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed',
|
||||
ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed',
|
||||
DIRECT_MESSAGE_SENT: '[Server] Direct Message Sent',
|
||||
GET_USER_INFO: '[Server] Get User Info',
|
||||
NOTIFY_USER: '[Server] Notify User',
|
||||
SERVER_SHUTDOWN: '[Server] Server Shutdown',
|
||||
|
|
@ -69,4 +68,6 @@ export const Types = {
|
|||
DECK_DEL_DIR: '[Server] Deck Del Dir',
|
||||
DECK_UPLOAD: '[Server] Deck Upload',
|
||||
DECK_DELETE: '[Server] Deck Delete',
|
||||
// User games
|
||||
GAMES_OF_USER: '[Server] Games Of User',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,3 +40,474 @@ export enum LeaveGameReason {
|
|||
USER_LEFT = 3,
|
||||
USER_DISCONNECTED = 4
|
||||
}
|
||||
|
||||
// ── Enums ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export enum ZoneType {
|
||||
PrivateZone = 0,
|
||||
PublicZone = 1,
|
||||
HiddenZone = 2,
|
||||
}
|
||||
|
||||
/** Matches CardAttribute enum in card_attributes.proto */
|
||||
export enum CardAttribute {
|
||||
AttrTapped = 1,
|
||||
AttrAttacking = 2,
|
||||
AttrFaceDown = 3,
|
||||
AttrColor = 4,
|
||||
AttrPT = 5,
|
||||
AttrAnnotation = 6,
|
||||
AttrDoesntUntap = 7,
|
||||
}
|
||||
|
||||
// ── Primitive data structures (mirrors ServerInfo_* protos) ──────────────────
|
||||
|
||||
export interface Color {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_CardCounter */
|
||||
export interface CardCounterInfo {
|
||||
id: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_Card */
|
||||
export interface CardInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
faceDown: boolean;
|
||||
tapped: boolean;
|
||||
attacking: boolean;
|
||||
color: string;
|
||||
pt: string;
|
||||
annotation: string;
|
||||
destroyOnZoneChange: boolean;
|
||||
doesntUntap: boolean;
|
||||
counterList: CardCounterInfo[];
|
||||
attachPlayerId: number;
|
||||
attachZone: string;
|
||||
attachCardId: number;
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_Zone */
|
||||
export interface ZoneInfo {
|
||||
name: string;
|
||||
type: ZoneType;
|
||||
withCoords: boolean;
|
||||
cardCount: number;
|
||||
cardList: CardInfo[];
|
||||
alwaysRevealTopCard: boolean;
|
||||
alwaysLookAtTopCard: boolean;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_Counter */
|
||||
export interface CounterInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
counterColor: Color;
|
||||
radius: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_Arrow */
|
||||
export interface ArrowInfo {
|
||||
id: number;
|
||||
startPlayerId: number;
|
||||
startZone: string;
|
||||
startCardId: number;
|
||||
targetPlayerId: number;
|
||||
targetZone: string;
|
||||
targetCardId: number;
|
||||
arrowColor: Color;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_PlayerProperties */
|
||||
export interface PlayerProperties {
|
||||
playerId: number;
|
||||
userInfo: any;
|
||||
spectator: boolean;
|
||||
conceded: boolean;
|
||||
readyStart: boolean;
|
||||
deckHash: string;
|
||||
pingSeconds: number;
|
||||
sideboardLocked: boolean;
|
||||
judge: boolean;
|
||||
}
|
||||
|
||||
/** Mirrors ServerInfo_Player */
|
||||
export interface PlayerInfo {
|
||||
properties: PlayerProperties;
|
||||
deckList: string;
|
||||
zoneList: ZoneInfo[];
|
||||
counterList: CounterInfo[];
|
||||
arrowList: ArrowInfo[];
|
||||
}
|
||||
|
||||
// ── Game event payload interfaces (data arriving from server events) ──────────
|
||||
|
||||
export interface GameStateChangedData {
|
||||
playerList: PlayerInfo[];
|
||||
gameStarted: boolean;
|
||||
activePlayerId: number;
|
||||
activePhase: number;
|
||||
secondsElapsed: number;
|
||||
}
|
||||
|
||||
export interface GameSayData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MoveCardData {
|
||||
cardId: number;
|
||||
cardName: string;
|
||||
startPlayerId: number;
|
||||
startZone: string;
|
||||
position: number;
|
||||
targetPlayerId: number;
|
||||
targetZone: string;
|
||||
x: number;
|
||||
y: number;
|
||||
newCardId: number;
|
||||
faceDown: boolean;
|
||||
newCardProviderId: string;
|
||||
}
|
||||
|
||||
export interface FlipCardData {
|
||||
zoneName: string;
|
||||
cardId: number;
|
||||
cardName: string;
|
||||
faceDown: boolean;
|
||||
cardProviderId: string;
|
||||
}
|
||||
|
||||
export interface DestroyCardData {
|
||||
zoneName: string;
|
||||
cardId: number;
|
||||
}
|
||||
|
||||
export interface AttachCardData {
|
||||
startZone: string;
|
||||
cardId: number;
|
||||
targetPlayerId: number;
|
||||
targetZone: string;
|
||||
targetCardId: number;
|
||||
}
|
||||
|
||||
export interface CreateTokenData {
|
||||
zoneName: string;
|
||||
cardId: number;
|
||||
cardName: string;
|
||||
color: string;
|
||||
pt: string;
|
||||
annotation: string;
|
||||
destroyOnZoneChange: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
cardProviderId: string;
|
||||
faceDown: boolean;
|
||||
}
|
||||
|
||||
export interface SetCardAttrData {
|
||||
zoneName: string;
|
||||
cardId: number;
|
||||
attribute: CardAttribute;
|
||||
attrValue: string;
|
||||
}
|
||||
|
||||
export interface SetCardCounterData {
|
||||
zoneName: string;
|
||||
cardId: number;
|
||||
counterId: number;
|
||||
counterValue: number;
|
||||
}
|
||||
|
||||
export interface CreateArrowData {
|
||||
arrowInfo: ArrowInfo;
|
||||
}
|
||||
|
||||
export interface DeleteArrowData {
|
||||
arrowId: number;
|
||||
}
|
||||
|
||||
export interface CreateCounterData {
|
||||
counterInfo: CounterInfo;
|
||||
}
|
||||
|
||||
export interface SetCounterData {
|
||||
counterId: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DelCounterData {
|
||||
counterId: number;
|
||||
}
|
||||
|
||||
export interface DrawCardsData {
|
||||
number: number;
|
||||
cards: CardInfo[];
|
||||
}
|
||||
|
||||
export interface RevealCardsData {
|
||||
zoneName: string;
|
||||
cardId: number[];
|
||||
otherPlayerId: number;
|
||||
cards: CardInfo[];
|
||||
grantWriteAccess: boolean;
|
||||
numberOfCards: number;
|
||||
}
|
||||
|
||||
export interface ShuffleData {
|
||||
zoneName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface RollDieData {
|
||||
sides: number;
|
||||
value: number;
|
||||
values: number[];
|
||||
}
|
||||
|
||||
export interface DumpZoneData {
|
||||
zoneOwnerId: number;
|
||||
zoneName: string;
|
||||
numberCards: number;
|
||||
isReversed: boolean;
|
||||
}
|
||||
|
||||
export interface ChangeZonePropertiesData {
|
||||
zoneName: string;
|
||||
alwaysRevealTopCard: boolean;
|
||||
alwaysLookAtTopCard: boolean;
|
||||
}
|
||||
|
||||
export interface SetActivePlayerData {
|
||||
activePlayerId: number;
|
||||
}
|
||||
|
||||
export interface SetActivePhaseData {
|
||||
phase: number;
|
||||
}
|
||||
|
||||
export interface ReverseTurnData {
|
||||
reversed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed to every game event handler alongside the event payload.
|
||||
* Contains per-container metadata from GameEventContainer.
|
||||
* Not stored in Redux — transient routing metadata only.
|
||||
*/
|
||||
export interface GameEventContext {
|
||||
'.Context_ReadyStart.ext'?: {};
|
||||
'.Context_Concede.ext'?: {};
|
||||
'.Context_DeckSelect.ext'?: {};
|
||||
'.Context_UndoDraw.ext'?: {};
|
||||
'.Context_MoveCard.ext'?: {};
|
||||
'.Context_Mulligan.ext'?: {};
|
||||
'.Context_PingChanged.ext'?: {};
|
||||
'.Context_ConnectionStateChanged.ext'?: {};
|
||||
'.Context_SetSideboardLock.ext'?: {};
|
||||
'.Context_Unconcede.ext'?: {};
|
||||
}
|
||||
|
||||
export interface GameEventMeta {
|
||||
gameId: number;
|
||||
playerId: number;
|
||||
/** Raw protobuf GameEventContext object. Not stored in Redux. */
|
||||
context: GameEventContext | null;
|
||||
secondsElapsed: number;
|
||||
/** Proto type is uint32. Non-zero means the action was forced by a judge. */
|
||||
forcedByJudge: number;
|
||||
}
|
||||
|
||||
// ── Command parameter interfaces ─────────────────────────────────────────────
|
||||
|
||||
export interface CardToMove {
|
||||
cardId: number;
|
||||
faceDown?: boolean;
|
||||
pt?: string;
|
||||
tapped?: boolean;
|
||||
}
|
||||
|
||||
export interface MoveCardParams {
|
||||
startPlayerId: number;
|
||||
startZone: string;
|
||||
cardsToMove: { card: CardToMove[] };
|
||||
targetPlayerId: number;
|
||||
targetZone: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
isReversed?: boolean;
|
||||
}
|
||||
|
||||
export interface DrawCardsParams {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export interface RollDieParams {
|
||||
sides: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface ShuffleParams {
|
||||
zoneName: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface FlipCardParams {
|
||||
zone: string;
|
||||
cardId: number;
|
||||
faceDown: boolean;
|
||||
pt?: string;
|
||||
}
|
||||
|
||||
export interface AttachCardParams {
|
||||
startZone: string;
|
||||
cardId: number;
|
||||
targetPlayerId?: number;
|
||||
targetZone?: string;
|
||||
targetCardId?: number;
|
||||
}
|
||||
|
||||
export interface CreateTokenParams {
|
||||
zone: string;
|
||||
cardName: string;
|
||||
color?: string;
|
||||
pt?: string;
|
||||
annotation?: string;
|
||||
destroyOnZoneChange?: boolean;
|
||||
x?: number;
|
||||
y?: number;
|
||||
targetZone?: string;
|
||||
targetCardId?: number;
|
||||
targetMode?: number;
|
||||
cardProviderId?: string;
|
||||
faceDown?: boolean;
|
||||
}
|
||||
|
||||
export interface SetCardAttrParams {
|
||||
zone: string;
|
||||
cardId: number;
|
||||
attribute: CardAttribute;
|
||||
attrValue: string;
|
||||
}
|
||||
|
||||
export interface SetCardCounterParams {
|
||||
zone: string;
|
||||
cardId: number;
|
||||
counterId: number;
|
||||
counterValue: number;
|
||||
}
|
||||
|
||||
export interface IncCardCounterParams {
|
||||
zone: string;
|
||||
cardId: number;
|
||||
counterId: number;
|
||||
counterDelta: number;
|
||||
}
|
||||
|
||||
export interface RevealCardsParams {
|
||||
zoneName: string;
|
||||
cardId?: number[];
|
||||
playerId?: number;
|
||||
grantWriteAccess?: boolean;
|
||||
topCards?: number;
|
||||
}
|
||||
|
||||
export interface DumpZoneParams {
|
||||
playerId: number;
|
||||
zoneName: string;
|
||||
numberCards: number;
|
||||
isReversed?: boolean;
|
||||
}
|
||||
|
||||
export interface ChangeZonePropertiesParams {
|
||||
zoneName: string;
|
||||
alwaysRevealTopCard?: boolean;
|
||||
alwaysLookAtTopCard?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateArrowParams {
|
||||
startPlayerId: number;
|
||||
startZone: string;
|
||||
startCardId: number;
|
||||
targetPlayerId: number;
|
||||
targetZone?: string;
|
||||
targetCardId?: number;
|
||||
arrowColor: Color;
|
||||
deleteInPhase?: number;
|
||||
}
|
||||
|
||||
export interface DeleteArrowParams {
|
||||
arrowId: number;
|
||||
}
|
||||
|
||||
export interface CreateCounterParams {
|
||||
counterName: string;
|
||||
counterColor: Color;
|
||||
radius: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface SetCounterParams {
|
||||
counterId: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface IncCounterParams {
|
||||
counterId: number;
|
||||
delta: number;
|
||||
}
|
||||
|
||||
export interface DelCounterParams {
|
||||
counterId: number;
|
||||
}
|
||||
|
||||
export interface KickFromGameParams {
|
||||
playerId: number;
|
||||
}
|
||||
|
||||
export interface ReadyStartParams {
|
||||
ready: boolean;
|
||||
forceStart?: boolean;
|
||||
}
|
||||
|
||||
export interface MulliganParams {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export interface DeckSelectParams {
|
||||
deck?: string;
|
||||
deckId?: number;
|
||||
}
|
||||
|
||||
export interface MoveCardToZone {
|
||||
cardName: string;
|
||||
startZone: string;
|
||||
targetZone: string;
|
||||
}
|
||||
|
||||
export interface SetSideboardPlanParams {
|
||||
moveList: MoveCardToZone[];
|
||||
}
|
||||
|
||||
export interface SetSideboardLockParams {
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
export interface SetActivePhaseParams {
|
||||
phase: number;
|
||||
}
|
||||
|
||||
export interface GameSayParams {
|
||||
message: string;
|
||||
}
|
||||
|
|
|
|||
2
webclient/src/vite-env.d.ts
vendored
Normal file
2
webclient/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vitest/globals" />
|
||||
|
|
@ -1,26 +1,28 @@
|
|||
jest.mock('./services/WebSocketService', () => ({
|
||||
WebSocketService: jest.fn().mockImplementation(() => ({
|
||||
message$: { subscribe: jest.fn() },
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
vi.mock('./services/WebSocketService', () => ({
|
||||
WebSocketService: vi.fn().mockImplementation(() => ({
|
||||
message$: { subscribe: vi.fn() },
|
||||
connect: vi.fn(),
|
||||
testConnect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('./services/ProtobufService', () => ({
|
||||
ProtobufService: jest.fn().mockImplementation(() => ({
|
||||
handleMessageEvent: jest.fn(),
|
||||
sendKeepAliveCommand: jest.fn(),
|
||||
resetCommands: jest.fn(),
|
||||
vi.mock('./services/ProtobufService', () => ({
|
||||
ProtobufService: vi.fn().mockImplementation(() => ({
|
||||
handleMessageEvent: vi.fn(),
|
||||
sendKeepAliveCommand: vi.fn(),
|
||||
resetCommands: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('./persistence', () => ({
|
||||
RoomPersistence: { clearStore: jest.fn() },
|
||||
SessionPersistence: { clearStore: jest.fn() },
|
||||
vi.mock('./persistence', () => ({
|
||||
RoomPersistence: { clearStore: vi.fn() },
|
||||
SessionPersistence: { clearStore: vi.fn() },
|
||||
}));
|
||||
|
||||
import { WebClient } from './WebClient';
|
||||
import { WebSocketService } from './services/WebSocketService';
|
||||
import { ProtobufService } from './services/ProtobufService';
|
||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||
import { StatusEnum } from 'types';
|
||||
import { Subject } from 'rxjs';
|
||||
|
|
@ -30,28 +32,26 @@ describe('WebClient', () => {
|
|||
let messageSubject: Subject<MessageEvent>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const { ProtobufService } = require('./services/ProtobufService');
|
||||
ProtobufService.mockImplementation(() => ({
|
||||
handleMessageEvent: jest.fn(),
|
||||
sendKeepAliveCommand: jest.fn(),
|
||||
resetCommands: jest.fn(),
|
||||
vi.clearAllMocks();
|
||||
(ProtobufService as vi.Mock).mockImplementation(() => ({
|
||||
handleMessageEvent: vi.fn(),
|
||||
sendKeepAliveCommand: vi.fn(),
|
||||
resetCommands: vi.fn(),
|
||||
}));
|
||||
messageSubject = new Subject<MessageEvent>();
|
||||
const { WebSocketService } = require('./services/WebSocketService');
|
||||
WebSocketService.mockImplementation(() => ({
|
||||
(WebSocketService as vi.Mock).mockImplementation(() => ({
|
||||
message$: messageSubject,
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
connect: vi.fn(),
|
||||
testConnect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
// suppress console.log from constructor in non-test-env check
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
client = new WebClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
|
|
@ -94,7 +94,7 @@ describe('WebClient', () => {
|
|||
|
||||
describe('keepAlive', () => {
|
||||
it('delegates to protobuf.sendKeepAliveCommand', () => {
|
||||
const pingCb = jest.fn();
|
||||
const pingCb = vi.fn();
|
||||
client.keepAlive(pingCb);
|
||||
expect(client.protobuf.sendKeepAliveCommand).toHaveBeenCalledWith(pingCb);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { StatusEnum, WebSocketConnectOptions } from 'types';
|
|||
import { ProtobufService } from './services/ProtobufService';
|
||||
import { WebSocketService } from './services/WebSocketService';
|
||||
|
||||
import { GameDispatch } from 'store';
|
||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||
|
||||
export class WebClient {
|
||||
|
|
@ -46,7 +47,7 @@ export class WebClient {
|
|||
this.protobuf.handleMessageEvent(message);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (import.meta.env.MODE !== 'test') {
|
||||
console.log(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +80,7 @@ export class WebClient {
|
|||
}
|
||||
|
||||
private clearStores() {
|
||||
GameDispatch.clearStore();
|
||||
RoomPersistence.clearStore();
|
||||
SessionPersistence.clearStore();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/**
|
||||
* Factory for invoking BackendService command callbacks in unit tests.
|
||||
*
|
||||
* @param mockFn - The jest.Mock for the BackendService send method
|
||||
* (e.g. BackendService.sendSessionCommand as jest.Mock).
|
||||
* @param mockFn - The vi.Mock for the BackendService send method
|
||||
* (e.g. BackendService.sendSessionCommand as vi.Mock).
|
||||
* @param optsArgIndex - Index of the options argument in the mock call.
|
||||
* Defaults to 2 (commandName, params, options).
|
||||
* Use 3 for sendRoomCommand (roomId, commandName, params, options).
|
||||
*/
|
||||
export function makeCallbackHelpers(mockFn: jest.Mock, optsArgIndex = 2) {
|
||||
export function makeCallbackHelpers(mockFn: vi.Mock, optsArgIndex = 2) {
|
||||
function getLastSendOpts() {
|
||||
const calls = mockFn.mock.calls;
|
||||
return calls[calls.length - 1]?.[optsArgIndex];
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@
|
|||
|
||||
/** Builds a minimal mock of ProtoController.root */
|
||||
export function makeMockProtoRoot() {
|
||||
const encode = { finish: jest.fn().mockReturnValue(new Uint8Array()) };
|
||||
const encode = { finish: vi.fn().mockReturnValue(new Uint8Array()) };
|
||||
return {
|
||||
CommandContainer: {
|
||||
create: jest.fn(args => ({ ...args })),
|
||||
encode: jest.fn().mockReturnValue(encode),
|
||||
create: vi.fn(args => ({ ...args })),
|
||||
encode: vi.fn().mockReturnValue(encode),
|
||||
},
|
||||
SessionCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
RoomCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
ModeratorCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
AdminCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
SessionCommand: { create: vi.fn(args => ({ ...args })) },
|
||||
RoomCommand: { create: vi.fn(args => ({ ...args })) },
|
||||
ModeratorCommand: { create: vi.fn(args => ({ ...args })) },
|
||||
AdminCommand: { create: vi.fn(args => ({ ...args })) },
|
||||
ServerMessage: {
|
||||
decode: jest.fn(),
|
||||
decode: vi.fn(),
|
||||
MessageType: {
|
||||
RESPONSE: 'RESPONSE',
|
||||
ROOM_EVENT: 'ROOM_EVENT',
|
||||
|
|
@ -52,8 +52,8 @@ export function makeMockProtoRoot() {
|
|||
/** Builds a mock WebSocket instance */
|
||||
export function makeMockWebSocketInstance() {
|
||||
return {
|
||||
send: jest.fn(),
|
||||
close: jest.fn(),
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
readyState: WebSocket.OPEN,
|
||||
binaryType: '' as BinaryType,
|
||||
onopen: null as any,
|
||||
|
|
@ -66,7 +66,7 @@ export function makeMockWebSocketInstance() {
|
|||
/** Installs a mock WebSocket constructor on global. Returns the mock instance. */
|
||||
export function installMockWebSocket() {
|
||||
const mockInstance = makeMockWebSocketInstance();
|
||||
const MockWS = jest.fn(() => mockInstance) as any;
|
||||
const MockWS = vi.fn(() => mockInstance) as any;
|
||||
MockWS.OPEN = 1;
|
||||
MockWS.CLOSED = 3;
|
||||
(global as any).WebSocket = MockWS;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Shared mock shape factories for session command specs.
|
||||
*
|
||||
* Usage inside jest.mock() factory callbacks (require is used because
|
||||
* jest.mock() is hoisted above imports):
|
||||
* Usage inside vi.mock() factory callbacks (require is used because
|
||||
* vi.mock() is hoisted above imports):
|
||||
*
|
||||
* jest.mock('../../WebClient', () => {
|
||||
* vi.mock('../../WebClient', () => {
|
||||
* const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
* return { __esModule: true, default: makeWebClientMock() };
|
||||
* });
|
||||
|
|
@ -13,10 +13,10 @@
|
|||
/** Superset WebClient mock — covers all properties used across both session spec files. */
|
||||
export function makeWebClientMock() {
|
||||
return {
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
connect: vi.fn(),
|
||||
testConnect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
clientConfig: { clientid: 'webatrice', clientver: '1.0', clientfeatures: [] },
|
||||
options: {},
|
||||
protocolVersion: 14,
|
||||
|
|
@ -60,73 +60,72 @@ export function makeProtoControllerRootMock() {
|
|||
/** Utils mock with unified return values. */
|
||||
export function makeUtilsMock() {
|
||||
return {
|
||||
hashPassword: jest.fn().mockReturnValue('hashed_pw'),
|
||||
generateSalt: jest.fn().mockReturnValue('randSalt'),
|
||||
passwordSaltSupported: jest.fn().mockReturnValue(0),
|
||||
hashPassword: vi.fn().mockReturnValue('hashed_pw'),
|
||||
generateSalt: vi.fn().mockReturnValue('randSalt'),
|
||||
passwordSaltSupported: vi.fn().mockReturnValue(0),
|
||||
};
|
||||
}
|
||||
|
||||
/** Superset SessionPersistence mock — covers all methods used across both session spec files. */
|
||||
export function makeSessionPersistenceMock() {
|
||||
return {
|
||||
loginSuccessful: jest.fn(),
|
||||
loginFailed: jest.fn(),
|
||||
updateBuddyList: jest.fn(),
|
||||
updateIgnoreList: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
updateUsers: jest.fn(),
|
||||
accountAwaitingActivation: jest.fn(),
|
||||
accountActivationSuccess: jest.fn(),
|
||||
accountActivationFailed: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
directMessageSent: jest.fn(),
|
||||
addToList: jest.fn(),
|
||||
removeFromList: jest.fn(),
|
||||
deleteServerDeck: jest.fn(),
|
||||
deleteServerDeckDir: jest.fn(),
|
||||
updateServerDecks: jest.fn(),
|
||||
uploadServerDeck: jest.fn(),
|
||||
createServerDeckDir: jest.fn(),
|
||||
getGamesOfUser: jest.fn(),
|
||||
getUserInfo: jest.fn(),
|
||||
accountPasswordChange: jest.fn(),
|
||||
accountEditChanged: jest.fn(),
|
||||
accountImageChanged: jest.fn(),
|
||||
replayList: jest.fn(),
|
||||
replayAdded: jest.fn(),
|
||||
replayModifyMatch: jest.fn(),
|
||||
replayDeleteMatch: jest.fn(),
|
||||
resetPasswordChallenge: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
resetPasswordFailed: jest.fn(),
|
||||
resetPasswordSuccess: jest.fn(),
|
||||
registrationFailed: jest.fn(),
|
||||
registrationSuccess: jest.fn(),
|
||||
registrationUserNameError: jest.fn(),
|
||||
registrationPasswordError: jest.fn(),
|
||||
registrationEmailError: jest.fn(),
|
||||
registrationRequiresEmail: jest.fn(),
|
||||
loginSuccessful: vi.fn(),
|
||||
loginFailed: vi.fn(),
|
||||
updateBuddyList: vi.fn(),
|
||||
updateIgnoreList: vi.fn(),
|
||||
updateUser: vi.fn(),
|
||||
updateUsers: vi.fn(),
|
||||
accountAwaitingActivation: vi.fn(),
|
||||
accountActivationSuccess: vi.fn(),
|
||||
accountActivationFailed: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
addToList: vi.fn(),
|
||||
removeFromList: vi.fn(),
|
||||
deleteServerDeck: vi.fn(),
|
||||
deleteServerDeckDir: vi.fn(),
|
||||
updateServerDecks: vi.fn(),
|
||||
uploadServerDeck: vi.fn(),
|
||||
createServerDeckDir: vi.fn(),
|
||||
getGamesOfUser: vi.fn(),
|
||||
getUserInfo: vi.fn(),
|
||||
accountPasswordChange: vi.fn(),
|
||||
accountEditChanged: vi.fn(),
|
||||
accountImageChanged: vi.fn(),
|
||||
replayList: vi.fn(),
|
||||
replayAdded: vi.fn(),
|
||||
replayModifyMatch: vi.fn(),
|
||||
replayDeleteMatch: vi.fn(),
|
||||
resetPasswordChallenge: vi.fn(),
|
||||
resetPassword: vi.fn(),
|
||||
resetPasswordFailed: vi.fn(),
|
||||
resetPasswordSuccess: vi.fn(),
|
||||
registrationFailed: vi.fn(),
|
||||
registrationSuccess: vi.fn(),
|
||||
registrationUserNameError: vi.fn(),
|
||||
registrationPasswordError: vi.fn(),
|
||||
registrationEmailError: vi.fn(),
|
||||
registrationRequiresEmail: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Session barrel mock — pure jest.fn() map for all cross-command calls.
|
||||
* Session barrel mock — pure vi.fn() map for all cross-command calls.
|
||||
* Used as-is by sessionCommands-complex.spec.ts, or spread over jest.requireActual
|
||||
* by sessionCommands-simple.spec.ts to preserve real implementations for
|
||||
* the commands under test.
|
||||
*/
|
||||
export function makeSessionBarrelMock() {
|
||||
return {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
activate: jest.fn(),
|
||||
forgotPasswordReset: jest.fn(),
|
||||
forgotPasswordRequest: jest.fn(),
|
||||
forgotPasswordChallenge: jest.fn(),
|
||||
requestPasswordSalt: jest.fn(),
|
||||
listUsers: jest.fn(),
|
||||
listRooms: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
login: vi.fn(),
|
||||
register: vi.fn(),
|
||||
activate: vi.fn(),
|
||||
forgotPasswordReset: vi.fn(),
|
||||
forgotPasswordRequest: vi.fn(),
|
||||
forgotPasswordChallenge: vi.fn(),
|
||||
requestPasswordSalt: vi.fn(),
|
||||
listUsers: vi.fn(),
|
||||
listRooms: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
jest.mock('../../services/BackendService', () => ({
|
||||
vi.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendAdminCommand: jest.fn(),
|
||||
sendAdminCommand: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => ({
|
||||
vi.mock('../../persistence', () => ({
|
||||
AdminPersistence: {
|
||||
adjustMod: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
shutdownServer: jest.fn(),
|
||||
updateServerMessage: jest.fn(),
|
||||
adjustMod: vi.fn(),
|
||||
reloadConfig: vi.fn(),
|
||||
shutdownServer: vi.fn(),
|
||||
updateServerMessage: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { AdminPersistence } from '../../persistence';
|
||||
import { adjustMod } from './adjustMod';
|
||||
import { reloadConfig } from './reloadConfig';
|
||||
import { shutdownServer } from './shutdownServer';
|
||||
import { updateServerMessage } from './updateServerMessage';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
|
||||
BackendService.sendAdminCommand as jest.Mock
|
||||
BackendService.sendAdminCommand as vi.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// adjustMod
|
||||
// ----------------------------------------------------------------
|
||||
describe('adjustMod', () => {
|
||||
const { adjustMod } = jest.requireActual('./adjustMod');
|
||||
|
||||
it('calls sendAdminCommand with Command_AdjustMod', () => {
|
||||
adjustMod('alice', true, false);
|
||||
|
|
@ -49,7 +52,6 @@ describe('adjustMod', () => {
|
|||
// reloadConfig
|
||||
// ----------------------------------------------------------------
|
||||
describe('reloadConfig', () => {
|
||||
const { reloadConfig } = jest.requireActual('./reloadConfig');
|
||||
|
||||
it('calls sendAdminCommand with Command_ReloadConfig', () => {
|
||||
reloadConfig();
|
||||
|
|
@ -67,7 +69,6 @@ describe('reloadConfig', () => {
|
|||
// shutdownServer
|
||||
// ----------------------------------------------------------------
|
||||
describe('shutdownServer', () => {
|
||||
const { shutdownServer } = jest.requireActual('./shutdownServer');
|
||||
|
||||
it('calls sendAdminCommand with Command_ShutdownServer', () => {
|
||||
shutdownServer('maintenance', 10);
|
||||
|
|
@ -89,7 +90,6 @@ describe('shutdownServer', () => {
|
|||
// updateServerMessage
|
||||
// ----------------------------------------------------------------
|
||||
describe('updateServerMessage', () => {
|
||||
const { updateServerMessage } = jest.requireActual('./updateServerMessage');
|
||||
|
||||
it('calls sendAdminCommand with Command_UpdateServerMessage', () => {
|
||||
updateServerMessage();
|
||||
|
|
|
|||
6
webclient/src/websocket/commands/game/attachCard.ts
Normal file
6
webclient/src/websocket/commands/game/attachCard.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { AttachCardParams } from 'types';
|
||||
|
||||
export function attachCard(gameId: number, params: AttachCardParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_AttachCard', params);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { ChangeZonePropertiesParams } from 'types';
|
||||
|
||||
export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_ChangeZoneProperties', params);
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/concede.ts
Normal file
5
webclient/src/websocket/commands/game/concede.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function concede(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_Concede', {});
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/createArrow.ts
Normal file
6
webclient/src/websocket/commands/game/createArrow.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { CreateArrowParams } from 'types';
|
||||
|
||||
export function createArrow(gameId: number, params: CreateArrowParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_CreateArrow', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/createCounter.ts
Normal file
6
webclient/src/websocket/commands/game/createCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { CreateCounterParams } from 'types';
|
||||
|
||||
export function createCounter(gameId: number, params: CreateCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_CreateCounter', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/createToken.ts
Normal file
6
webclient/src/websocket/commands/game/createToken.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { CreateTokenParams } from 'types';
|
||||
|
||||
export function createToken(gameId: number, params: CreateTokenParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_CreateToken', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/deckSelect.ts
Normal file
6
webclient/src/websocket/commands/game/deckSelect.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { DeckSelectParams } from 'types';
|
||||
|
||||
export function deckSelect(gameId: number, params: DeckSelectParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_DeckSelect', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/delCounter.ts
Normal file
6
webclient/src/websocket/commands/game/delCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { DelCounterParams } from 'types';
|
||||
|
||||
export function delCounter(gameId: number, params: DelCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_DelCounter', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/deleteArrow.ts
Normal file
6
webclient/src/websocket/commands/game/deleteArrow.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { DeleteArrowParams } from 'types';
|
||||
|
||||
export function deleteArrow(gameId: number, params: DeleteArrowParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_DeleteArrow', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/drawCards.ts
Normal file
6
webclient/src/websocket/commands/game/drawCards.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { DrawCardsParams } from 'types';
|
||||
|
||||
export function drawCards(gameId: number, params: DrawCardsParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_DrawCards', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/dumpZone.ts
Normal file
6
webclient/src/websocket/commands/game/dumpZone.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { DumpZoneParams } from 'types';
|
||||
|
||||
export function dumpZone(gameId: number, params: DumpZoneParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_DumpZone', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/flipCard.ts
Normal file
6
webclient/src/websocket/commands/game/flipCard.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { FlipCardParams } from 'types';
|
||||
|
||||
export function flipCard(gameId: number, params: FlipCardParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_FlipCard', params);
|
||||
}
|
||||
217
webclient/src/websocket/commands/game/gameCommands.spec.ts
Normal file
217
webclient/src/websocket/commands/game/gameCommands.spec.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { attachCard } from './attachCard';
|
||||
import { changeZoneProperties } from './changeZoneProperties';
|
||||
import { concede } from './concede';
|
||||
import { createArrow } from './createArrow';
|
||||
import { createCounter } from './createCounter';
|
||||
import { createToken } from './createToken';
|
||||
import { deckSelect } from './deckSelect';
|
||||
import { delCounter } from './delCounter';
|
||||
import { deleteArrow } from './deleteArrow';
|
||||
import { drawCards } from './drawCards';
|
||||
import { dumpZone } from './dumpZone';
|
||||
import { flipCard } from './flipCard';
|
||||
import { gameSay } from './gameSay';
|
||||
import { incCardCounter } from './incCardCounter';
|
||||
import { incCounter } from './incCounter';
|
||||
import { kickFromGame } from './kickFromGame';
|
||||
import { leaveGame } from './leaveGame';
|
||||
import { moveCard } from './moveCard';
|
||||
import { mulligan } from './mulligan';
|
||||
import { nextTurn } from './nextTurn';
|
||||
import { readyStart } from './readyStart';
|
||||
import { revealCards } from './revealCards';
|
||||
import { reverseTurn } from './reverseTurn';
|
||||
import { setActivePhase } from './setActivePhase';
|
||||
import { setCardAttr } from './setCardAttr';
|
||||
import { setCardCounter } from './setCardCounter';
|
||||
import { setCounter } from './setCounter';
|
||||
import { setSideboardLock } from './setSideboardLock';
|
||||
import { setSideboardPlan } from './setSideboardPlan';
|
||||
import { shuffle } from './shuffle';
|
||||
import { undoDraw } from './undoDraw';
|
||||
import { unconcede } from './unconcede';
|
||||
import { judge } from './judge';
|
||||
|
||||
vi.mock('../../services/BackendService', () => ({
|
||||
BackendService: { sendGameCommand: vi.fn() },
|
||||
}));
|
||||
|
||||
const gameId = 1;
|
||||
const params = {} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
(BackendService.sendGameCommand as vi.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('Game commands — delegate to BackendService.sendGameCommand', () => {
|
||||
it('attachCard sends Command_AttachCard', () => {
|
||||
attachCard(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_AttachCard', params);
|
||||
});
|
||||
|
||||
it('changeZoneProperties sends Command_ChangeZoneProperties', () => {
|
||||
changeZoneProperties(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ChangeZoneProperties', params);
|
||||
});
|
||||
|
||||
it('concede sends Command_Concede with empty object', () => {
|
||||
concede(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Concede', {});
|
||||
});
|
||||
|
||||
it('createArrow sends Command_CreateArrow', () => {
|
||||
createArrow(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateArrow', params);
|
||||
});
|
||||
|
||||
it('createCounter sends Command_CreateCounter', () => {
|
||||
createCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateCounter', params);
|
||||
});
|
||||
|
||||
it('createToken sends Command_CreateToken', () => {
|
||||
createToken(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateToken', params);
|
||||
});
|
||||
|
||||
it('deckSelect sends Command_DeckSelect', () => {
|
||||
deckSelect(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DeckSelect', params);
|
||||
});
|
||||
|
||||
it('delCounter sends Command_DelCounter', () => {
|
||||
delCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DelCounter', params);
|
||||
});
|
||||
|
||||
it('deleteArrow sends Command_DeleteArrow', () => {
|
||||
deleteArrow(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DeleteArrow', params);
|
||||
});
|
||||
|
||||
it('drawCards sends Command_DrawCards', () => {
|
||||
drawCards(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DrawCards', params);
|
||||
});
|
||||
|
||||
it('dumpZone sends Command_DumpZone', () => {
|
||||
dumpZone(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DumpZone', params);
|
||||
});
|
||||
|
||||
it('flipCard sends Command_FlipCard', () => {
|
||||
flipCard(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_FlipCard', params);
|
||||
});
|
||||
|
||||
it('gameSay sends Command_GameSay', () => {
|
||||
gameSay(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_GameSay', params);
|
||||
});
|
||||
|
||||
it('incCardCounter sends Command_IncCardCounter', () => {
|
||||
incCardCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_IncCardCounter', params);
|
||||
});
|
||||
|
||||
it('incCounter sends Command_IncCounter', () => {
|
||||
incCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_IncCounter', params);
|
||||
});
|
||||
|
||||
it('kickFromGame sends Command_KickFromGame', () => {
|
||||
kickFromGame(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_KickFromGame', params);
|
||||
});
|
||||
|
||||
it('leaveGame sends Command_LeaveGame with empty object', () => {
|
||||
leaveGame(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_LeaveGame', {});
|
||||
});
|
||||
|
||||
it('moveCard sends Command_MoveCard', () => {
|
||||
moveCard(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_MoveCard', params);
|
||||
});
|
||||
|
||||
it('mulligan sends Command_Mulligan', () => {
|
||||
mulligan(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Mulligan', params);
|
||||
});
|
||||
|
||||
it('nextTurn sends Command_NextTurn with empty object', () => {
|
||||
nextTurn(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_NextTurn', {});
|
||||
});
|
||||
|
||||
it('readyStart sends Command_ReadyStart', () => {
|
||||
readyStart(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ReadyStart', params);
|
||||
});
|
||||
|
||||
it('revealCards sends Command_RevealCards', () => {
|
||||
revealCards(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_RevealCards', params);
|
||||
});
|
||||
|
||||
it('reverseTurn sends Command_ReverseTurn with empty object', () => {
|
||||
reverseTurn(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ReverseTurn', {});
|
||||
});
|
||||
|
||||
it('setActivePhase sends Command_SetActivePhase', () => {
|
||||
setActivePhase(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetActivePhase', params);
|
||||
});
|
||||
|
||||
it('setCardAttr sends Command_SetCardAttr', () => {
|
||||
setCardAttr(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCardAttr', params);
|
||||
});
|
||||
|
||||
it('setCardCounter sends Command_SetCardCounter', () => {
|
||||
setCardCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCardCounter', params);
|
||||
});
|
||||
|
||||
it('setCounter sends Command_SetCounter', () => {
|
||||
setCounter(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCounter', params);
|
||||
});
|
||||
|
||||
it('setSideboardLock sends Command_SetSideboardLock', () => {
|
||||
setSideboardLock(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetSideboardLock', params);
|
||||
});
|
||||
|
||||
it('setSideboardPlan sends Command_SetSideboardPlan', () => {
|
||||
setSideboardPlan(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetSideboardPlan', params);
|
||||
});
|
||||
|
||||
it('shuffle sends Command_Shuffle', () => {
|
||||
shuffle(gameId, params);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Shuffle', params);
|
||||
});
|
||||
|
||||
it('undoDraw sends Command_UndoDraw with empty object', () => {
|
||||
undoDraw(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_UndoDraw', {});
|
||||
});
|
||||
|
||||
it('unconcede sends Command_Unconcede with empty object', () => {
|
||||
unconcede(gameId);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Unconcede', {});
|
||||
});
|
||||
|
||||
it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => {
|
||||
const targetId = 3;
|
||||
const innerGameCommand = { '.Command_DrawCards.ext': { numberOfCards: 2 } };
|
||||
judge(gameId, targetId, innerGameCommand);
|
||||
expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Judge', {
|
||||
targetId,
|
||||
gameCommand: [innerGameCommand],
|
||||
});
|
||||
});
|
||||
});
|
||||
6
webclient/src/websocket/commands/game/gameSay.ts
Normal file
6
webclient/src/websocket/commands/game/gameSay.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { GameSayParams } from 'types';
|
||||
|
||||
export function gameSay(gameId: number, params: GameSayParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_GameSay', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/incCardCounter.ts
Normal file
6
webclient/src/websocket/commands/game/incCardCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { IncCardCounterParams } from 'types';
|
||||
|
||||
export function incCardCounter(gameId: number, params: IncCardCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_IncCardCounter', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/incCounter.ts
Normal file
6
webclient/src/websocket/commands/game/incCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { IncCounterParams } from 'types';
|
||||
|
||||
export function incCounter(gameId: number, params: IncCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_IncCounter', params);
|
||||
}
|
||||
33
webclient/src/websocket/commands/game/index.ts
Normal file
33
webclient/src/websocket/commands/game/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export { leaveGame } from './leaveGame';
|
||||
export { kickFromGame } from './kickFromGame';
|
||||
export { gameSay } from './gameSay';
|
||||
export { readyStart } from './readyStart';
|
||||
export { concede } from './concede';
|
||||
export { unconcede } from './unconcede';
|
||||
export { judge } from './judge';
|
||||
export { nextTurn } from './nextTurn';
|
||||
export { setActivePhase } from './setActivePhase';
|
||||
export { reverseTurn } from './reverseTurn';
|
||||
export { moveCard } from './moveCard';
|
||||
export { flipCard } from './flipCard';
|
||||
export { attachCard } from './attachCard';
|
||||
export { createToken } from './createToken';
|
||||
export { setCardAttr } from './setCardAttr';
|
||||
export { setCardCounter } from './setCardCounter';
|
||||
export { incCardCounter } from './incCardCounter';
|
||||
export { drawCards } from './drawCards';
|
||||
export { undoDraw } from './undoDraw';
|
||||
export { createArrow } from './createArrow';
|
||||
export { deleteArrow } from './deleteArrow';
|
||||
export { createCounter } from './createCounter';
|
||||
export { setCounter } from './setCounter';
|
||||
export { incCounter } from './incCounter';
|
||||
export { delCounter } from './delCounter';
|
||||
export { shuffle } from './shuffle';
|
||||
export { dumpZone } from './dumpZone';
|
||||
export { revealCards } from './revealCards';
|
||||
export { changeZoneProperties } from './changeZoneProperties';
|
||||
export { deckSelect } from './deckSelect';
|
||||
export { setSideboardPlan } from './setSideboardPlan';
|
||||
export { setSideboardLock } from './setSideboardLock';
|
||||
export { mulligan } from './mulligan';
|
||||
8
webclient/src/websocket/commands/game/judge.ts
Normal file
8
webclient/src/websocket/commands/game/judge.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function judge(gameId: number, targetId: number, innerGameCommand: any): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_Judge', {
|
||||
targetId,
|
||||
gameCommand: [innerGameCommand],
|
||||
});
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/kickFromGame.ts
Normal file
6
webclient/src/websocket/commands/game/kickFromGame.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { KickFromGameParams } from 'types';
|
||||
|
||||
export function kickFromGame(gameId: number, params: KickFromGameParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_KickFromGame', params);
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/leaveGame.ts
Normal file
5
webclient/src/websocket/commands/game/leaveGame.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function leaveGame(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_LeaveGame', {});
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/moveCard.ts
Normal file
6
webclient/src/websocket/commands/game/moveCard.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { MoveCardParams } from 'types';
|
||||
|
||||
export function moveCard(gameId: number, params: MoveCardParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_MoveCard', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/mulligan.ts
Normal file
6
webclient/src/websocket/commands/game/mulligan.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { MulliganParams } from 'types';
|
||||
|
||||
export function mulligan(gameId: number, params: MulliganParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_Mulligan', params);
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/nextTurn.ts
Normal file
5
webclient/src/websocket/commands/game/nextTurn.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function nextTurn(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_NextTurn', {});
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/readyStart.ts
Normal file
6
webclient/src/websocket/commands/game/readyStart.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { ReadyStartParams } from 'types';
|
||||
|
||||
export function readyStart(gameId: number, params: ReadyStartParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_ReadyStart', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/revealCards.ts
Normal file
6
webclient/src/websocket/commands/game/revealCards.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { RevealCardsParams } from 'types';
|
||||
|
||||
export function revealCards(gameId: number, params: RevealCardsParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_RevealCards', params);
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/reverseTurn.ts
Normal file
5
webclient/src/websocket/commands/game/reverseTurn.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function reverseTurn(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_ReverseTurn', {});
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/setActivePhase.ts
Normal file
6
webclient/src/websocket/commands/game/setActivePhase.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetActivePhaseParams } from 'types';
|
||||
|
||||
export function setActivePhase(gameId: number, params: SetActivePhaseParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetActivePhase', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/setCardAttr.ts
Normal file
6
webclient/src/websocket/commands/game/setCardAttr.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetCardAttrParams } from 'types';
|
||||
|
||||
export function setCardAttr(gameId: number, params: SetCardAttrParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetCardAttr', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/setCardCounter.ts
Normal file
6
webclient/src/websocket/commands/game/setCardCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetCardCounterParams } from 'types';
|
||||
|
||||
export function setCardCounter(gameId: number, params: SetCardCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetCardCounter', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/setCounter.ts
Normal file
6
webclient/src/websocket/commands/game/setCounter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetCounterParams } from 'types';
|
||||
|
||||
export function setCounter(gameId: number, params: SetCounterParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetCounter', params);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetSideboardLockParams } from 'types';
|
||||
|
||||
export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetSideboardLock', params);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { SetSideboardPlanParams } from 'types';
|
||||
|
||||
export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_SetSideboardPlan', params);
|
||||
}
|
||||
6
webclient/src/websocket/commands/game/shuffle.ts
Normal file
6
webclient/src/websocket/commands/game/shuffle.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
import { ShuffleParams } from 'types';
|
||||
|
||||
export function shuffle(gameId: number, params: ShuffleParams): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_Shuffle', params);
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/unconcede.ts
Normal file
5
webclient/src/websocket/commands/game/unconcede.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function unconcede(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_Unconcede', {});
|
||||
}
|
||||
5
webclient/src/websocket/commands/game/undoDraw.ts
Normal file
5
webclient/src/websocket/commands/game/undoDraw.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { BackendService } from '../../services/BackendService';
|
||||
|
||||
export function undoDraw(gameId: number): void {
|
||||
BackendService.sendGameCommand(gameId, 'Command_UndoDraw', {});
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * as AdminCommands from './admin';
|
||||
export * as GameCommands from './game';
|
||||
export * as ModeratorCommands from './moderator';
|
||||
export * as RoomCommands from './room';
|
||||
export * as SessionCommands from './session';
|
||||
|
|
|
|||
|
|
@ -1,39 +1,48 @@
|
|||
jest.mock('../../services/BackendService', () => ({
|
||||
vi.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendModeratorCommand: jest.fn(),
|
||||
sendModeratorCommand: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => ({
|
||||
vi.mock('../../persistence', () => ({
|
||||
ModeratorPersistence: {
|
||||
banFromServer: jest.fn(),
|
||||
forceActivateUser: jest.fn(),
|
||||
getAdminNotes: jest.fn(),
|
||||
banHistory: jest.fn(),
|
||||
warnHistory: jest.fn(),
|
||||
warnListOptions: jest.fn(),
|
||||
grantReplayAccess: jest.fn(),
|
||||
updateAdminNotes: jest.fn(),
|
||||
viewLogs: jest.fn(),
|
||||
warnUser: jest.fn(),
|
||||
banFromServer: vi.fn(),
|
||||
forceActivateUser: vi.fn(),
|
||||
getAdminNotes: vi.fn(),
|
||||
banHistory: vi.fn(),
|
||||
warnHistory: vi.fn(),
|
||||
warnListOptions: vi.fn(),
|
||||
grantReplayAccess: vi.fn(),
|
||||
updateAdminNotes: vi.fn(),
|
||||
viewLogs: vi.fn(),
|
||||
warnUser: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { ModeratorPersistence } from '../../persistence';
|
||||
import { banFromServer } from './banFromServer';
|
||||
import { forceActivateUser } from './forceActivateUser';
|
||||
import { getAdminNotes } from './getAdminNotes';
|
||||
import { getBanHistory } from './getBanHistory';
|
||||
import { getWarnHistory } from './getWarnHistory';
|
||||
import { getWarnList } from './getWarnList';
|
||||
import { grantReplayAccess } from './grantReplayAccess';
|
||||
import { updateAdminNotes } from './updateAdminNotes';
|
||||
import { viewLogHistory } from './viewLogHistory';
|
||||
import { warnUser } from './warnUser';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
|
||||
BackendService.sendModeratorCommand as jest.Mock
|
||||
BackendService.sendModeratorCommand as vi.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// banFromServer
|
||||
// ----------------------------------------------------------------
|
||||
describe('banFromServer', () => {
|
||||
const { banFromServer } = jest.requireActual('./banFromServer');
|
||||
|
||||
it('calls sendModeratorCommand with Command_BanFromServer', () => {
|
||||
banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1);
|
||||
|
|
@ -55,7 +64,6 @@ describe('banFromServer', () => {
|
|||
// forceActivateUser
|
||||
// ----------------------------------------------------------------
|
||||
describe('forceActivateUser', () => {
|
||||
const { forceActivateUser } = jest.requireActual('./forceActivateUser');
|
||||
|
||||
it('calls sendModeratorCommand with Command_ForceActivateUser', () => {
|
||||
forceActivateUser('alice', 'mod1');
|
||||
|
|
@ -73,7 +81,6 @@ describe('forceActivateUser', () => {
|
|||
// getAdminNotes
|
||||
// ----------------------------------------------------------------
|
||||
describe('getAdminNotes', () => {
|
||||
const { getAdminNotes } = jest.requireActual('./getAdminNotes');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetAdminNotes', () => {
|
||||
getAdminNotes('alice');
|
||||
|
|
@ -96,7 +103,6 @@ describe('getAdminNotes', () => {
|
|||
// getBanHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('getBanHistory', () => {
|
||||
const { getBanHistory } = jest.requireActual('./getBanHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetBanHistory', () => {
|
||||
getBanHistory('alice');
|
||||
|
|
@ -119,7 +125,6 @@ describe('getBanHistory', () => {
|
|||
// getWarnHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('getWarnHistory', () => {
|
||||
const { getWarnHistory } = jest.requireActual('./getWarnHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetWarnHistory', () => {
|
||||
getWarnHistory('alice');
|
||||
|
|
@ -142,7 +147,6 @@ describe('getWarnHistory', () => {
|
|||
// getWarnList
|
||||
// ----------------------------------------------------------------
|
||||
describe('getWarnList', () => {
|
||||
const { getWarnList } = jest.requireActual('./getWarnList');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetWarnList', () => {
|
||||
getWarnList('mod1', 'alice', 'US');
|
||||
|
|
@ -165,7 +169,6 @@ describe('getWarnList', () => {
|
|||
// grantReplayAccess
|
||||
// ----------------------------------------------------------------
|
||||
describe('grantReplayAccess', () => {
|
||||
const { grantReplayAccess } = jest.requireActual('./grantReplayAccess');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GrantReplayAccess', () => {
|
||||
grantReplayAccess(10, 'mod1');
|
||||
|
|
@ -183,7 +186,6 @@ describe('grantReplayAccess', () => {
|
|||
// updateAdminNotes
|
||||
// ----------------------------------------------------------------
|
||||
describe('updateAdminNotes', () => {
|
||||
const { updateAdminNotes } = jest.requireActual('./updateAdminNotes');
|
||||
|
||||
it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => {
|
||||
updateAdminNotes('alice', 'new notes');
|
||||
|
|
@ -201,7 +203,6 @@ describe('updateAdminNotes', () => {
|
|||
// viewLogHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('viewLogHistory', () => {
|
||||
const { viewLogHistory } = jest.requireActual('./viewLogHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_ViewLogHistory', () => {
|
||||
viewLogHistory({ filters: 'all' } as any);
|
||||
|
|
@ -224,7 +225,6 @@ describe('viewLogHistory', () => {
|
|||
// warnUser
|
||||
// ----------------------------------------------------------------
|
||||
describe('warnUser', () => {
|
||||
const { warnUser } = jest.requireActual('./warnUser');
|
||||
|
||||
it('calls sendModeratorCommand with Command_WarnUser', () => {
|
||||
warnUser('alice', 'bad behavior', 'cid');
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue