mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-06 13:33:55 -07:00
replace websocket with sockatrice
This commit is contained in:
parent
68050b56bc
commit
e2319c84db
218 changed files with 89 additions and 9100 deletions
2
webclient/.gitignore
vendored
2
webclient/.gitignore
vendored
|
|
@ -9,9 +9,7 @@
|
||||||
/plans
|
/plans
|
||||||
|
|
||||||
# generated ./src files
|
# generated ./src files
|
||||||
/src/proto-files.json
|
|
||||||
/src/server-props.json
|
/src/server-props.json
|
||||||
/src/generated/
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
/**
|
|
||||||
* Custom protoc-gen-es sibling plugin. Emits `src/generated/index.ts`, a
|
|
||||||
* single rollup that re-exports every generated `_pb` module and adds
|
|
||||||
* `MessageInitShape<T>` param aliases for every `Command_*` message.
|
|
||||||
*
|
|
||||||
* Wired into `buf.gen.yaml` as a second local plugin. Runs with the same
|
|
||||||
* descriptor set protoc-gen-es consumes, so output always tracks the protos.
|
|
||||||
*/
|
|
||||||
import { createEcmaScriptPlugin, runNodeJs } from '@bufbuild/protoplugin';
|
|
||||||
|
|
||||||
const HEADER = [
|
|
||||||
'// @generated by protoc-gen-data. DO NOT EDIT.',
|
|
||||||
'// Rollup of all proto modules + MessageInitShape param aliases for every Command_*,',
|
|
||||||
'// plus type maps for Response/Event extensions grouped by scope.',
|
|
||||||
'/* eslint-disable */',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const inner = createEcmaScriptPlugin({
|
|
||||||
name: 'protoc-gen-data',
|
|
||||||
version: 'v0.1.0',
|
|
||||||
generateTs(schema) {
|
|
||||||
const f = schema.generateFile('index.ts');
|
|
||||||
|
|
||||||
const MessageInitShape = f.import('MessageInitShape', '@bufbuild/protobuf', true);
|
|
||||||
const MessageType = f.import('Message', '@bufbuild/protobuf', true);
|
|
||||||
const GenExtensionType = f.import('GenExtension', '@bufbuild/protobuf/codegenv2', true);
|
|
||||||
|
|
||||||
const sortedFiles = [...schema.files].sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
|
||||||
f.print('export * from ', f.string(`./proto/${file.name}_pb`), ';');
|
|
||||||
}
|
|
||||||
f.print();
|
|
||||||
|
|
||||||
const commandMessages = [];
|
|
||||||
for (const file of sortedFiles) {
|
|
||||||
for (const msg of file.messages) {
|
|
||||||
if (msg.name.startsWith('Command_')) {
|
|
||||||
commandMessages.push(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commandMessages.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
// importSchema() resolves paths relative to this plugin's `out` dir, which
|
|
||||||
// yields `./<name>_pb` — but the _pb files live under ./proto/ (protoc-gen-es's
|
|
||||||
// out). Build the import path explicitly so it points inside the proto subdir.
|
|
||||||
for (const msg of commandMessages) {
|
|
||||||
const alias = msg.name.slice('Command_'.length) + 'Params';
|
|
||||||
const schemaName = `${msg.name}Schema`;
|
|
||||||
const schemaSym = f.import(schemaName, `./proto/${msg.file.name}_pb`, true);
|
|
||||||
f.print('export type ', alias, ' = ', MessageInitShape, '<typeof ', schemaSym, '>;');
|
|
||||||
}
|
|
||||||
f.print();
|
|
||||||
|
|
||||||
// ── Type maps for Response/Event extensions, grouped by extendee ────────
|
|
||||||
//
|
|
||||||
// Scans all messages for nested `extend` declarations and groups them by
|
|
||||||
// which message they extend (Response, SessionEvent, RoomEvent, GameEvent).
|
|
||||||
// Emits one `interface *Map { TypeName: TypeName; ... }` per scope.
|
|
||||||
|
|
||||||
/** @type {Map<string, import('@bufbuild/protobuf').DescMessage[]>} */
|
|
||||||
const extendeeGroups = new Map();
|
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
|
||||||
for (const msg of file.messages) {
|
|
||||||
for (const ext of msg.nestedExtensions) {
|
|
||||||
const target = ext.extendee.name;
|
|
||||||
const group = extendeeGroups.get(target);
|
|
||||||
if (group) {
|
|
||||||
group.push(msg);
|
|
||||||
} else {
|
|
||||||
extendeeGroups.set(target, [msg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {[string, string, import('@bufbuild/protobuf').DescMessage | null][]} */
|
|
||||||
const maps = [
|
|
||||||
['ResponseMap', 'Response', null],
|
|
||||||
['SessionEventMap', 'SessionEvent', null],
|
|
||||||
['RoomEventMap', 'RoomEvent', null],
|
|
||||||
['GameEventMap', 'GameEvent', null],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Resolve the base extendee message for maps that need the base type included
|
|
||||||
for (const file of sortedFiles) {
|
|
||||||
for (const msg of file.messages) {
|
|
||||||
for (const entry of maps) {
|
|
||||||
if (msg.name === entry[1]) {
|
|
||||||
entry[2] = msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [mapName, extendeeName, baseMsg] of maps) {
|
|
||||||
const msgs = (extendeeGroups.get(extendeeName) || []).slice();
|
|
||||||
msgs.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
if (msgs.length === 0 && !baseMsg) continue;
|
|
||||||
|
|
||||||
f.print('export interface ', mapName, ' {');
|
|
||||||
|
|
||||||
// Include the base extendee type itself (e.g. Response in ResponseMap)
|
|
||||||
if (baseMsg) {
|
|
||||||
const sym = f.import(baseMsg.name, `./proto/${baseMsg.file.name}_pb`, true);
|
|
||||||
f.print(' ', baseMsg.name, ': ', sym, ';');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const msg of msgs) {
|
|
||||||
const sym = f.import(msg.name, `./proto/${msg.file.name}_pb`, true);
|
|
||||||
f.print(' ', msg.name, ': ', sym, ';');
|
|
||||||
}
|
|
||||||
|
|
||||||
f.print('}');
|
|
||||||
f.print();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic extension registry infrastructure. Consolidates the three
|
|
||||||
// near-duplicate registry types and helpers that used to live in
|
|
||||||
// src/websocket/services/protobuf-types.ts into one generic pair.
|
|
||||||
// Specialised aliases (Session/Room/Game) still live in protobuf-types.ts
|
|
||||||
// because GameExtensionRegistry needs GameEventMeta — a hand-written
|
|
||||||
// domain type whose import would create a generated/ ↔ types/ cycle.
|
|
||||||
f.print('export type RegistryEntry<V, T extends ', MessageType, ', M = unknown> = [');
|
|
||||||
f.print(' ', GenExtensionType, '<T, V>,');
|
|
||||||
f.print(' (value: V, meta: M) => void,');
|
|
||||||
f.print('];');
|
|
||||||
f.print();
|
|
||||||
// Return type widens V to `unknown` so the heterogeneous entries that
|
|
||||||
// callers build can be stored in a homogeneous `RegistryEntry<unknown, T, M>[]`
|
|
||||||
// array. This is the actual value-add over a bare tuple literal.
|
|
||||||
f.print('export function makeEntry<T extends ', MessageType, ', V, M = unknown>(');
|
|
||||||
f.print(' ext: ', GenExtensionType, '<T, V>,');
|
|
||||||
f.print(' handler: (value: V, meta: M) => void,');
|
|
||||||
f.print('): RegistryEntry<unknown, T, M> {');
|
|
||||||
f.print(' return [ext, handler] as unknown as RegistryEntry<unknown, T, M>;');
|
|
||||||
f.print('}');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Skip f.preamble() above and inject a custom rollup-aware header here instead —
|
|
||||||
// preamble() would write "@generated from file X.proto" which is misleading for
|
|
||||||
// a rollup file built from every input proto.
|
|
||||||
/** @type {import('@bufbuild/protoplugin').Plugin} */
|
|
||||||
const plugin = {
|
|
||||||
name: inner.name,
|
|
||||||
version: inner.version,
|
|
||||||
run(request) {
|
|
||||||
const response = inner.run(request);
|
|
||||||
for (const file of response.file) {
|
|
||||||
if (file.name === 'index.ts' && typeof file.content === 'string') {
|
|
||||||
file.content = HEADER + file.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
runNodeJs(plugin);
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
version: v2
|
|
||||||
inputs:
|
|
||||||
- directory: ../libcockatrice_protocol/libcockatrice/protocol/pb
|
|
||||||
plugins:
|
|
||||||
- local: protoc-gen-es
|
|
||||||
out: src/generated/proto
|
|
||||||
opt:
|
|
||||||
- target=ts
|
|
||||||
- local: [node, buf.gen.plugin.mjs]
|
|
||||||
out: src/generated
|
|
||||||
opt:
|
|
||||||
- target=ts
|
|
||||||
|
|
@ -6,43 +6,37 @@ const elements = [
|
||||||
{ type: 'containers', pattern: ['src/containers/**'] },
|
{ type: 'containers', pattern: ['src/containers/**'] },
|
||||||
{ type: 'dialogs', pattern: ['src/dialogs/**'] },
|
{ type: 'dialogs', pattern: ['src/dialogs/**'] },
|
||||||
{ type: 'forms', pattern: ['src/forms/**'] },
|
{ type: 'forms', pattern: ['src/forms/**'] },
|
||||||
{ type: 'generated', pattern: ['src/generated/**'] },
|
|
||||||
{ type: 'hooks', pattern: ['src/hooks/**'] },
|
{ type: 'hooks', pattern: ['src/hooks/**'] },
|
||||||
{ type: 'images', pattern: ['src/images/**'] },
|
{ type: 'images', pattern: ['src/images/**'] },
|
||||||
{ type: 'services', pattern: ['src/services/**'] },
|
{ type: 'services', pattern: ['src/services/**'] },
|
||||||
{ type: 'store', pattern: ['src/store/**'] },
|
{ type: 'store', pattern: ['src/store/**'] },
|
||||||
{ type: 'types', pattern: ['src/types/**'] },
|
{ type: 'types', pattern: ['src/types/**'] },
|
||||||
{ type: 'utils', pattern: ['src/utils/**'] },
|
{ type: 'utils', pattern: ['src/utils/**'] },
|
||||||
{ type: 'websocket-types', pattern: ['src/websocket/types/**'] },
|
|
||||||
{ type: 'websocket', pattern: ['src/websocket/**'] },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const types = (...types) => types.map((type) => ({ to: { type } }));
|
const types = (...types) => types.map((type) => ({ to: { type } }));
|
||||||
|
|
||||||
const rules = [
|
const rules = [
|
||||||
{ from: { type: 'generated' }, allow: [] },
|
{ from: { type: 'types' }, allow: [] },
|
||||||
{ from: { type: 'websocket-types' }, allow: types('generated') },
|
|
||||||
{ from: { type: 'websocket' }, allow: types('generated', 'websocket-types') },
|
|
||||||
{ from: { type: 'types' }, allow: types('generated') },
|
|
||||||
{ from: { type: 'utils' }, allow: types('types') },
|
{ from: { type: 'utils' }, allow: types('types') },
|
||||||
|
|
||||||
{ from: { type: 'store' }, allow: types('types', 'utils', 'websocket-types') },
|
{ from: { type: 'store' }, allow: types('types', 'utils') },
|
||||||
{ from: { type: 'api' }, allow: types('store', 'types', 'utils', 'websocket', 'websocket-types') },
|
{ from: { type: 'api' }, allow: types('store', 'types', 'utils') },
|
||||||
|
|
||||||
{ from: { type: 'images' }, allow: types('types') },
|
{ from: { type: 'images' }, allow: types('types') },
|
||||||
{ from: { type: 'services' }, allow: types('api', 'store', 'types', 'utils') },
|
{ from: { type: 'services' }, allow: types('api', 'store', 'types', 'utils') },
|
||||||
{ from: { type: 'hooks' }, allow: types('api', 'services', 'store', 'types', 'utils', 'websocket', 'websocket-types') },
|
{ from: { type: 'hooks' }, allow: types('api', 'services', 'store', 'types', 'utils') },
|
||||||
|
|
||||||
{
|
{
|
||||||
from: { type: 'components' },
|
from: { type: 'components' },
|
||||||
allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils', 'websocket-types')
|
allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: { type: 'containers' },
|
from: { type: 'containers' },
|
||||||
allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils', 'websocket-types')
|
allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils')
|
||||||
},
|
},
|
||||||
{ from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'store', 'types', 'utils', 'websocket-types') },
|
{ from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'store', 'types', 'utils') },
|
||||||
{ from: { type: 'forms' }, allow: types('components', 'hooks', 'services', 'store', 'types', 'utils', 'websocket-types') },
|
{ from: { type: 'forms' }, allow: types('components', 'hooks', 'services', 'store', 'types', 'utils') },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const boundariesConfig = [
|
export const boundariesConfig = [
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { boundariesConfig } from './eslint.boundaries.mjs';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
// Global ignores
|
// Global ignores
|
||||||
{ ignores: ['node_modules/**', 'build/**', 'public/pb/**', 'src/generated/**'] },
|
{ ignores: ['node_modules/**', 'build/**'] },
|
||||||
|
|
||||||
// Base JS recommended
|
// Base JS recommended
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,14 @@ import { ServerDispatch, RoomsDispatch, GameDispatch } from '@app/store';
|
||||||
import { Data } from '@app/types';
|
import { Data } from '@app/types';
|
||||||
import { WebClient, setPendingOptions } from '@app/websocket';
|
import { WebClient, setPendingOptions } from '@app/websocket';
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
import { WebsocketTypes } from '@app/websocket/types';
|
||||||
import { PROTOCOL_VERSION } from '../../../src/websocket/config';
|
import {
|
||||||
import { createWebClientRequest, createWebClientResponse } from '@app/api';
|
CLIENT_CONFIG,
|
||||||
|
CLIENT_OPTIONS,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
createWebClientResponse,
|
||||||
|
} from '@app/api';
|
||||||
|
|
||||||
|
export { PROTOCOL_VERSION };
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildResponse,
|
buildResponse,
|
||||||
|
|
@ -195,7 +201,12 @@ installMockWebSocket();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
new WebClient(createWebClientRequest(), createWebClientResponse());
|
new WebClient(
|
||||||
|
createWebClientResponse(),
|
||||||
|
CLIENT_CONFIG,
|
||||||
|
CLIENT_OPTIONS,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ import { Data } from '@app/types';
|
||||||
import { store } from '@app/store';
|
import { store } from '@app/store';
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
import { WebsocketTypes } from '@app/websocket/types';
|
||||||
|
|
||||||
import { PROTOCOL_VERSION } from '../../../src/websocket/config';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMockWebSocket,
|
getMockWebSocket,
|
||||||
getWebClient,
|
getWebClient,
|
||||||
openMockWebSocket,
|
openMockWebSocket,
|
||||||
setPendingOptions,
|
setPendingOptions,
|
||||||
connectAndHandshake,
|
connectAndHandshake,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
} from '../helpers/setup';
|
} from '../helpers/setup';
|
||||||
import {
|
import {
|
||||||
buildSessionEventMessage,
|
buildSessionEventMessage,
|
||||||
|
|
|
||||||
242
webclient/package-lock.json
generated
242
webclient/package-lock.json
generated
|
|
@ -18,7 +18,6 @@
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dexie": "^4.4.2",
|
"dexie": "^4.4.2",
|
||||||
"dompurify": "^3.4.0",
|
|
||||||
"final-form": "^5.0.0",
|
"final-form": "^5.0.0",
|
||||||
"final-form-set-field-touched": "^1.0.1",
|
"final-form-set-field-touched": "^1.0.1",
|
||||||
"i18next": "^26.0.5",
|
"i18next": "^26.0.5",
|
||||||
|
|
@ -35,18 +34,16 @@
|
||||||
"react-router-dom": "^7.14.1",
|
"react-router-dom": "^7.14.1",
|
||||||
"react-virtualized-auto-sizer": "^2.0.3",
|
"react-virtualized-auto-sizer": "^2.0.3",
|
||||||
"react-window": "^2.2.7",
|
"react-window": "^2.2.7",
|
||||||
"rxjs": "^7.5.4"
|
"rxjs": "^7.5.4",
|
||||||
|
"sockatrice": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@bufbuild/buf": "^1.68.1",
|
|
||||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@mui/types": "^9.0.0",
|
"@mui/types": "^9.0.0",
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "^1.51.0",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.4.0",
|
"@testing-library/jest-dom": "^6.4.0",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
|
||||||
"@types/node": "^22.19.17",
|
"@types/node": "^22.19.17",
|
||||||
"@types/prop-types": "^15.7.4",
|
"@types/prop-types": "^15.7.4",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
|
|
@ -299,207 +296,12 @@
|
||||||
"specificity": "bin/cli.js"
|
"specificity": "bin/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bufbuild/buf": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-QDJ3oy4qZ5EVS2JYtmpE1n9FuaoABthxIddXB050huGddatr1sjHJSSAXXpLotOI18pW3KQ4zzU1x5Ms+pEEOw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"buf": "bin/buf",
|
|
||||||
"protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking",
|
|
||||||
"protoc-gen-buf-lint": "bin/protoc-gen-buf-lint"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@bufbuild/buf-darwin-arm64": "1.68.1",
|
|
||||||
"@bufbuild/buf-darwin-x64": "1.68.1",
|
|
||||||
"@bufbuild/buf-linux-aarch64": "1.68.1",
|
|
||||||
"@bufbuild/buf-linux-armv7": "1.68.1",
|
|
||||||
"@bufbuild/buf-linux-x64": "1.68.1",
|
|
||||||
"@bufbuild/buf-win32-arm64": "1.68.1",
|
|
||||||
"@bufbuild/buf-win32-x64": "1.68.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-darwin-arm64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-+Cu/2Kr6Add3s+Zk/edcF9QdpnrsukQkdR/z4fk4+qr6YZqfWfiV8f+s14I3h7qPrPnGeCeynvmZ9NmJ1BMYuA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-darwin-x64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-hvAs452aJ6io9hZKSfr3TvC+//16zW5y5u3ucsIXVkl5mkmKWSCkPbZwGpjNCfRGGUsyRJGL6rixxTgLKw2k5w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-linux-aarch64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-GLCakHzZVKUPlAiJEPGMBLW+yBk8tuz6NNcoeQU5lB5AO7ks8V8x9cy4CQjne4YSl3niF1JtvAQckLKhEWPueQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-linux-armv7": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-lUMCULl3MOYQe0oAPWnqNVYy8pL+F3Jeq6C4sSY+0E9udaACMc2mZ32gYingaMop9O1qS58HjJbezFxxL+CJqg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-linux-x64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-eRU3UWiZQthAgx+qFTG3EeJ/VeOcZzAkKYGt5ansOnOIJHBm+3RG2KqA+Jm8q3EFqB1XpVcGxPXnIu/qmFJXaQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-win32-arm64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-v3xlKzs3l2C+mYv+T0sYol05DTmsFKYmM5Vz8+AyrXdjxRwq2QH7m0arVWwxHX2MwyhQxKA+qqjoF8bCUM7xxA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/buf-win32-x64": {
|
|
||||||
"version": "1.68.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.68.1.tgz",
|
|
||||||
"integrity": "sha512-b62pwu+G7n5tF8n1QIoT85K7xgKJZS8SzdN020weOa7IVvMNHCDqMq7nrkz46fXCkK7MtD1YJ6sUp86sWSZPsw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/protobuf": {
|
"node_modules/@bufbuild/protobuf": {
|
||||||
"version": "2.11.0",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
},
|
},
|
||||||
"node_modules/@bufbuild/protoc-gen-es": {
|
|
||||||
"version": "2.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.11.0.tgz",
|
|
||||||
"integrity": "sha512-VzQuwEQDXipbZ1soWUuAWm1Z0C3B/IDWGeysnbX6ogJ6As91C2mdvAND/ekQ4YIWgen4d5nqLfIBOWLqCCjYUA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@bufbuild/protobuf": "2.11.0",
|
|
||||||
"@bufbuild/protoplugin": "2.11.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"protoc-gen-es": "bin/protoc-gen-es"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@bufbuild/protobuf": "2.11.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@bufbuild/protobuf": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/protoplugin": {
|
|
||||||
"version": "2.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz",
|
|
||||||
"integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@bufbuild/protobuf": "2.11.0",
|
|
||||||
"@typescript/vfs": "^1.6.2",
|
|
||||||
"typescript": "5.4.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bufbuild/protoplugin/node_modules/typescript": {
|
|
||||||
"version": "5.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
|
||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@csstools/color-helpers": {
|
"node_modules/@csstools/color-helpers": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||||
|
|
@ -2063,16 +1865,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/dompurify": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/trusted-types": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/esrecurse": {
|
"node_modules/@types/esrecurse": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||||
|
|
@ -2169,8 +1961,8 @@
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"devOptional": true,
|
"license": "MIT",
|
||||||
"license": "MIT"
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
|
|
@ -2421,19 +2213,6 @@
|
||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript/vfs": {
|
|
||||||
"version": "1.6.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz",
|
|
||||||
"integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.4.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
|
||||||
|
|
@ -5593,6 +5372,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sockatrice": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-xywXolumrums59NteojtdmVKfjF8Ah71ewseczecZBqSmNAQ6zhM9enhmukWLvgQJr0rh7o1RDX5m+54kct+vA==",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/protobuf": "^2.11.0",
|
||||||
|
"dompurify": "^3.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,10 @@
|
||||||
"test:e2e:down": "docker compose -f e2e/docker/docker-compose.e2e.yml down -v",
|
"test:e2e:down": "docker compose -f e2e/docker/docker-compose.e2e.yml down -v",
|
||||||
"test:e2e:install-browsers": "npx playwright install --with-deps chromium",
|
"test:e2e:install-browsers": "npx playwright install --with-deps chromium",
|
||||||
"pretest:e2e": "npm run test:e2e:install-browsers",
|
"pretest:e2e": "npm run test:e2e:install-browsers",
|
||||||
"prebuild": "npm run proto:generate && node prebuild.js",
|
"prebuild": "node prebuild.js",
|
||||||
"prepare": "cd .. && husky",
|
"prepare": "cd .. && husky",
|
||||||
"prestart": "npm run proto:generate && node prebuild.js",
|
"prestart": "node prebuild.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"proto:generate": "npx buf generate",
|
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"translate": "node prebuild.js -i18nOnly"
|
"translate": "node prebuild.js -i18nOnly"
|
||||||
},
|
},
|
||||||
|
|
@ -44,7 +43,6 @@
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dexie": "^4.4.2",
|
"dexie": "^4.4.2",
|
||||||
"dompurify": "^3.4.0",
|
|
||||||
"final-form": "^5.0.0",
|
"final-form": "^5.0.0",
|
||||||
"final-form-set-field-touched": "^1.0.1",
|
"final-form-set-field-touched": "^1.0.1",
|
||||||
"i18next": "^26.0.5",
|
"i18next": "^26.0.5",
|
||||||
|
|
@ -61,11 +59,10 @@
|
||||||
"react-router-dom": "^7.14.1",
|
"react-router-dom": "^7.14.1",
|
||||||
"react-virtualized-auto-sizer": "^2.0.3",
|
"react-virtualized-auto-sizer": "^2.0.3",
|
||||||
"react-window": "^2.2.7",
|
"react-window": "^2.2.7",
|
||||||
"rxjs": "^7.5.4"
|
"rxjs": "^7.5.4",
|
||||||
|
"sockatrice": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@bufbuild/buf": "^1.68.1",
|
|
||||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "^1.51.0",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.12",
|
||||||
|
|
@ -73,7 +70,6 @@
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.4.0",
|
"@testing-library/jest-dom": "^6.4.0",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
|
||||||
"@types/node": "^22.19.17",
|
"@types/node": "^22.19.17",
|
||||||
"@types/prop-types": "^15.7.4",
|
"@types/prop-types": "^15.7.4",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
|
|
|
||||||
4
webclient/public/pb/.gitignore
vendored
4
webclient/public/pb/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
# Ignore all files
|
|
||||||
*
|
|
||||||
# Except gitignore
|
|
||||||
!.gitignore
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
|
||||||
import { configureStore, Reducer } from '@reduxjs/toolkit';
|
import { configureStore, Reducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { WebClientContext } from '../hooks/useWebClient';
|
import { WebClientContext } from '../hooks/useWebClient';
|
||||||
import type { WebClient } from '../websocket';
|
import type { WebClient } from '@app/websocket';
|
||||||
import { createMockWebClient } from './mockWebClient';
|
import { createMockWebClient } from './mockWebClient';
|
||||||
|
|
||||||
// Minimal Provider wrapper for hook-only tests. Use this instead of
|
// Minimal Provider wrapper for hook-only tests. Use this instead of
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function createMockWebClient() {
|
||||||
accountEdit: vi.fn(),
|
accountEdit: vi.fn(),
|
||||||
accountPassword: vi.fn(),
|
accountPassword: vi.fn(),
|
||||||
accountImage: vi.fn(),
|
accountImage: vi.fn(),
|
||||||
listUsers: vi.fn(),
|
message: vi.fn(),
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
joinRoom: vi.fn(),
|
joinRoom: vi.fn(),
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const testTheme = createTheme({
|
||||||
});
|
});
|
||||||
|
|
||||||
import { WebClientContext } from '../hooks/useWebClient';
|
import { WebClientContext } from '../hooks/useWebClient';
|
||||||
import type { WebClient } from '../websocket';
|
import type { WebClient } from '@app/websocket';
|
||||||
import rootReducer from '../store/rootReducer';
|
import rootReducer from '../store/rootReducer';
|
||||||
import { ToastProvider } from '../components/Toast/ToastContext';
|
import { ToastProvider } from '../components/Toast/ToastContext';
|
||||||
import { storeMiddlewareOptions } from '../store/store';
|
import { storeMiddlewareOptions } from '../store/store';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import type { WebsocketTypes } from '@app/websocket/types';
|
||||||
|
|
||||||
export const PROTOCOL_VERSION = 14;
|
export const PROTOCOL_VERSION = 14;
|
||||||
|
|
||||||
export const CLIENT_CONFIG = {
|
export const CLIENT_CONFIG: WebsocketTypes.ClientConfig = {
|
||||||
clientid: 'webatrice',
|
clientid: 'webatrice',
|
||||||
clientver: 'webclient-1.0 (2019-10-31)',
|
clientver: 'webclient-1.0 (2019-10-31)',
|
||||||
clientfeatures: [
|
clientfeatures: [
|
||||||
|
|
@ -9,19 +11,17 @@ export const CLIENT_CONFIG = {
|
||||||
'feature_set',
|
'feature_set',
|
||||||
'room_chat_history',
|
'room_chat_history',
|
||||||
'client_warnings',
|
'client_warnings',
|
||||||
/* unimplemented features */
|
|
||||||
'forgot_password',
|
'forgot_password',
|
||||||
'idle_client',
|
'idle_client',
|
||||||
'mod_log_lookup',
|
'mod_log_lookup',
|
||||||
'user_ban_history',
|
'user_ban_history',
|
||||||
// satisfy server reqs for POC
|
|
||||||
'websocket',
|
'websocket',
|
||||||
'2.7.0_min_version',
|
'2.7.0_min_version',
|
||||||
'2.8.0_min_version'
|
'2.8.0_min_version',
|
||||||
]
|
],
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
export const CLIENT_OPTIONS = {
|
export const CLIENT_OPTIONS: WebsocketTypes.ClientOptions = {
|
||||||
autojoinrooms: true,
|
autojoinrooms: true,
|
||||||
keepalive: 5000
|
keepalive: 5000,
|
||||||
} as const;
|
};
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export { createWebClientResponse } from './response';
|
export { createWebClientResponse } from './response';
|
||||||
export { createWebClientRequest } from './request';
|
export { CLIENT_CONFIG, CLIENT_OPTIONS, PROTOCOL_VERSION } from './clientConfig';
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { AdminCommands } from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
export class AdminRequestImpl implements WebsocketTypes.IAdminRequest {
|
|
||||||
adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void {
|
|
||||||
AdminCommands.adjustMod(userName, shouldBeMod, shouldBeJudge);
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadConfig(): void {
|
|
||||||
AdminCommands.reloadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownServer(reason: string, minutes: number): void {
|
|
||||||
AdminCommands.shutdownServer(reason, minutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateServerMessage(): void {
|
|
||||||
AdminCommands.updateServerMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import {
|
|
||||||
WebClient,
|
|
||||||
SessionCommands,
|
|
||||||
setPendingOptions,
|
|
||||||
} from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
interface AppAuthRequestOverrides extends WebsocketTypes.AuthRequestMap {
|
|
||||||
LoginParams: Omit<WebsocketTypes.LoginConnectOptions, 'reason'>;
|
|
||||||
ConnectTarget: Omit<WebsocketTypes.TestConnectionOptions, 'reason'>;
|
|
||||||
RegisterParams: Omit<WebsocketTypes.RegisterConnectOptions, 'reason'>;
|
|
||||||
ActivateParams: Omit<WebsocketTypes.ActivateConnectOptions, 'reason'>;
|
|
||||||
ForgotPasswordRequestParams: Omit<WebsocketTypes.PasswordResetRequestConnectOptions, 'reason'>;
|
|
||||||
ForgotPasswordChallengeParams: Omit<WebsocketTypes.PasswordResetChallengeConnectOptions, 'reason'>;
|
|
||||||
ForgotPasswordResetParams: Omit<WebsocketTypes.PasswordResetConnectOptions, 'reason'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONNECTING_STATUS_LABEL = 'Connecting...';
|
|
||||||
|
|
||||||
function beginConnect(
|
|
||||||
options: { host: string; port: string | number },
|
|
||||||
reason: WebsocketTypes.WebSocketConnectReason,
|
|
||||||
): void {
|
|
||||||
setPendingOptions({ ...options, reason });
|
|
||||||
SessionCommands.updateStatus(WebsocketTypes.StatusEnum.CONNECTING, CONNECTING_STATUS_LABEL);
|
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuthenticationRequestImpl implements WebsocketTypes.IAuthenticationRequest<AppAuthRequestOverrides> {
|
|
||||||
login(options: Omit<WebsocketTypes.LoginConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.LOGIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
testConnection(options: Omit<WebsocketTypes.TestConnectionOptions, 'reason'>): void {
|
|
||||||
WebClient.instance.testConnect({ host: options.host, port: options.port });
|
|
||||||
}
|
|
||||||
|
|
||||||
register(options: Omit<WebsocketTypes.RegisterConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.REGISTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
activateAccount(options: Omit<WebsocketTypes.ActivateConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPasswordRequest(options: Omit<WebsocketTypes.PasswordResetRequestConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPasswordChallenge(options: Omit<WebsocketTypes.PasswordResetChallengeConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword(options: Omit<WebsocketTypes.PasswordResetConnectOptions, 'reason'>): void {
|
|
||||||
beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect(): void {
|
|
||||||
SessionCommands.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
import { GameCommands } from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
import { Data } from '@app/types';
|
|
||||||
|
|
||||||
export class GameRequestImpl implements WebsocketTypes.IGameRequest {
|
|
||||||
leaveGame(gameId: number): void {
|
|
||||||
GameCommands.leaveGame(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
kickFromGame(gameId: number, params: Data.KickFromGameParams): void {
|
|
||||||
GameCommands.kickFromGame(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
gameSay(gameId: number, params: Data.GameSayParams): void {
|
|
||||||
GameCommands.gameSay(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
readyStart(gameId: number, params: Data.ReadyStartParams): void {
|
|
||||||
GameCommands.readyStart(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
concede(gameId: number): void {
|
|
||||||
GameCommands.concede(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
unconcede(gameId: number): void {
|
|
||||||
GameCommands.unconcede(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void {
|
|
||||||
GameCommands.judge(gameId, targetId, innerGameCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTurn(gameId: number): void {
|
|
||||||
GameCommands.nextTurn(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void {
|
|
||||||
GameCommands.setActivePhase(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
reverseTurn(gameId: number): void {
|
|
||||||
GameCommands.reverseTurn(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveCard(gameId: number, params: Data.MoveCardParams): void {
|
|
||||||
GameCommands.moveCard(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
flipCard(gameId: number, params: Data.FlipCardParams): void {
|
|
||||||
GameCommands.flipCard(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
attachCard(gameId: number, params: Data.AttachCardParams): void {
|
|
||||||
GameCommands.attachCard(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
createToken(gameId: number, params: Data.CreateTokenParams): void {
|
|
||||||
GameCommands.createToken(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCardAttr(gameId: number, params: Data.SetCardAttrParams): void {
|
|
||||||
GameCommands.setCardAttr(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCardCounter(gameId: number, params: Data.SetCardCounterParams): void {
|
|
||||||
GameCommands.setCardCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
incCardCounter(gameId: number, params: Data.IncCardCounterParams): void {
|
|
||||||
GameCommands.incCardCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCards(gameId: number, params: Data.DrawCardsParams): void {
|
|
||||||
GameCommands.drawCards(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
undoDraw(gameId: number): void {
|
|
||||||
GameCommands.undoDraw(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
createArrow(gameId: number, params: Data.CreateArrowParams): void {
|
|
||||||
GameCommands.createArrow(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteArrow(gameId: number, params: Data.DeleteArrowParams): void {
|
|
||||||
GameCommands.deleteArrow(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
createCounter(gameId: number, params: Data.CreateCounterParams): void {
|
|
||||||
GameCommands.createCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCounter(gameId: number, params: Data.SetCounterParams): void {
|
|
||||||
GameCommands.setCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
incCounter(gameId: number, params: Data.IncCounterParams): void {
|
|
||||||
GameCommands.incCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
delCounter(gameId: number, params: Data.DelCounterParams): void {
|
|
||||||
GameCommands.delCounter(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle(gameId: number, params: Data.ShuffleParams): void {
|
|
||||||
GameCommands.shuffle(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpZone(gameId: number, params: Data.DumpZoneParams): void {
|
|
||||||
GameCommands.dumpZone(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
revealCards(gameId: number, params: Data.RevealCardsParams): void {
|
|
||||||
GameCommands.revealCards(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void {
|
|
||||||
GameCommands.changeZoneProperties(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
deckSelect(gameId: number, params: Data.DeckSelectParams): void {
|
|
||||||
GameCommands.deckSelect(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void {
|
|
||||||
GameCommands.setSideboardPlan(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void {
|
|
||||||
GameCommands.setSideboardLock(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
mulligan(gameId: number, params: Data.MulliganParams): void {
|
|
||||||
GameCommands.mulligan(gameId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
rollDie(gameId: number, params: Data.RollDieParams): void {
|
|
||||||
GameCommands.rollDie(gameId, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { Data } from '@app/types';
|
|
||||||
import { ModeratorCommands } from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
export class ModeratorRequestImpl implements WebsocketTypes.IModeratorRequest {
|
|
||||||
banFromServer(
|
|
||||||
minutes: number,
|
|
||||||
userName?: string,
|
|
||||||
address?: string,
|
|
||||||
reason?: string,
|
|
||||||
visibleReason?: string,
|
|
||||||
clientid?: string,
|
|
||||||
removeMessages?: number
|
|
||||||
): void {
|
|
||||||
ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
forceActivateUser(usernameToActivate: string, moderatorName: string): void {
|
|
||||||
ModeratorCommands.forceActivateUser(usernameToActivate, moderatorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAdminNotes(userName: string): void {
|
|
||||||
ModeratorCommands.getAdminNotes(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBanHistory(userName: string): void {
|
|
||||||
ModeratorCommands.getBanHistory(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWarnHistory(userName: string): void {
|
|
||||||
ModeratorCommands.getWarnHistory(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWarnList(modName: string, userName: string, userClientid: string): void {
|
|
||||||
ModeratorCommands.getWarnList(modName, userName, userClientid);
|
|
||||||
}
|
|
||||||
|
|
||||||
grantReplayAccess(replayId: number, moderatorName: string): void {
|
|
||||||
ModeratorCommands.grantReplayAccess(replayId, moderatorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAdminNotes(userName: string, notes: string): void {
|
|
||||||
ModeratorCommands.updateAdminNotes(userName, notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLogHistory(filters: Data.ViewLogHistoryParams): void {
|
|
||||||
ModeratorCommands.viewLogHistory(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void {
|
|
||||||
ModeratorCommands.warnUser(userName, reason, clientid, removeMessages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { RoomCommands, SessionCommands } from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
import type { App } from '@app/types';
|
|
||||||
|
|
||||||
export class RoomsRequestImpl implements WebsocketTypes.IRoomsRequest {
|
|
||||||
joinRoom(roomId: number): void {
|
|
||||||
SessionCommands.joinRoom(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveRoom(roomId: number): void {
|
|
||||||
RoomCommands.leaveRoom(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
roomSay(roomId: number, message: string): void {
|
|
||||||
RoomCommands.roomSay(roomId, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
createGame(roomId: number, params: App.CreateGameParams): void {
|
|
||||||
RoomCommands.createGame(roomId, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
joinGame(roomId: number, params: App.JoinGameParams): void {
|
|
||||||
RoomCommands.joinGame(roomId, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { SessionCommands } from '@app/websocket';
|
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
export class SessionRequestImpl implements WebsocketTypes.ISessionRequest {
|
|
||||||
addToBuddyList(userName: string): void {
|
|
||||||
SessionCommands.addToBuddyList(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromBuddyList(userName: string): void {
|
|
||||||
SessionCommands.removeFromBuddyList(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToIgnoreList(userName: string): void {
|
|
||||||
SessionCommands.addToIgnoreList(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromIgnoreList(userName: string): void {
|
|
||||||
SessionCommands.removeFromIgnoreList(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void {
|
|
||||||
SessionCommands.accountPassword(oldPassword, newPassword, hashedNewPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void {
|
|
||||||
SessionCommands.accountEdit(passwordCheck, realName, email, country);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeAccountImage(image: Uint8Array): void {
|
|
||||||
SessionCommands.accountImage(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendDirectMessage(userName: string, message: string): void {
|
|
||||||
SessionCommands.message(userName, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserInfo(userName: string): void {
|
|
||||||
SessionCommands.getUserInfo(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserGames(userName: string): void {
|
|
||||||
SessionCommands.getGamesOfUser(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
deckDownload(deckId: number): void {
|
|
||||||
SessionCommands.deckDownload(deckId);
|
|
||||||
}
|
|
||||||
|
|
||||||
replayDownload(replayId: number): void {
|
|
||||||
SessionCommands.replayDownload(replayId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { WebsocketTypes } from '@app/websocket/types';
|
|
||||||
|
|
||||||
import { AuthenticationRequestImpl } from './AuthenticationRequestImpl';
|
|
||||||
import { SessionRequestImpl } from './SessionRequestImpl';
|
|
||||||
import { RoomsRequestImpl } from './RoomsRequestImpl';
|
|
||||||
import { GameRequestImpl } from './GameRequestImpl';
|
|
||||||
import { AdminRequestImpl } from './AdminRequestImpl';
|
|
||||||
import { ModeratorRequestImpl } from './ModeratorRequestImpl';
|
|
||||||
|
|
||||||
export { AuthenticationRequestImpl, SessionRequestImpl, RoomsRequestImpl, GameRequestImpl, AdminRequestImpl, ModeratorRequestImpl };
|
|
||||||
|
|
||||||
export function createWebClientRequest(): WebsocketTypes.IWebClientRequest {
|
|
||||||
return {
|
|
||||||
authentication: new AuthenticationRequestImpl(),
|
|
||||||
session: new SessionRequestImpl(),
|
|
||||||
rooms: new RoomsRequestImpl(),
|
|
||||||
game: new GameRequestImpl(),
|
|
||||||
admin: new AdminRequestImpl(),
|
|
||||||
moderator: new ModeratorRequestImpl(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -60,7 +60,7 @@ export function usePlayer(): PlayerViewModel {
|
||||||
const onRemoveBuddy = () => name && webClient.request.session.removeFromBuddyList(name);
|
const onRemoveBuddy = () => name && webClient.request.session.removeFromBuddyList(name);
|
||||||
const onAddIgnore = () => name && webClient.request.session.addToIgnoreList(name);
|
const onAddIgnore = () => name && webClient.request.session.addToIgnoreList(name);
|
||||||
const onRemoveIgnore = () => name && webClient.request.session.removeFromIgnoreList(name);
|
const onRemoveIgnore = () => name && webClient.request.session.removeFromIgnoreList(name);
|
||||||
const onSendMessage = (message: string) => name && webClient.request.session.sendDirectMessage(name, message);
|
const onSendMessage = (message: string) => name && webClient.request.session.message(name, message);
|
||||||
const onWarnUser = (reason: string) => name && webClient.request.moderator.warnUser(name, reason);
|
const onWarnUser = (reason: string) => name && webClient.request.moderator.warnUser(name, reason);
|
||||||
const onBanFromServer = (minutes: number, reason: string, visibleReason?: string) =>
|
const onBanFromServer = (minutes: number, reason: string, visibleReason?: string) =>
|
||||||
name && webClient.request.moderator.banFromServer(minutes, name, undefined, reason, visibleReason);
|
name && webClient.request.moderator.banFromServer(minutes, name, undefined, reason, visibleReason);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const Rooms = ({ rooms, joinedRooms }: RoomsProps) => {
|
||||||
if (joinedRoomIds.has(roomId)) {
|
if (joinedRoomIds.has(roomId)) {
|
||||||
navigate(generatePath(App.RouteEnum.ROOM, { roomId: String(roomId) }));
|
navigate(generatePath(App.RouteEnum.ROOM, { roomId: String(roomId) }));
|
||||||
} else {
|
} else {
|
||||||
webClient.request.rooms.joinRoom(roomId);
|
webClient.request.session.joinRoom(roomId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ vi.mock('@app/websocket', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@app/api', () => ({
|
vi.mock('@app/api', () => ({
|
||||||
createWebClientRequest: vi.fn(() => 'request'),
|
|
||||||
createWebClientResponse: vi.fn(() => 'response'),
|
createWebClientResponse: vi.fn(() => 'response'),
|
||||||
|
CLIENT_CONFIG: { clientid: 'test', clientver: 'test', clientfeatures: [] },
|
||||||
|
CLIENT_OPTIONS: { autojoinrooms: false, keepalive: 5000 },
|
||||||
|
PROTOCOL_VERSION: 14,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function Wrapper({ children }: { children: ReactNode }) {
|
function Wrapper({ children }: { children: ReactNode }) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
import { WebClient } from '@app/websocket';
|
import { WebClient } from '@app/websocket';
|
||||||
import { createWebClientRequest, createWebClientResponse } from '@app/api';
|
import {
|
||||||
|
CLIENT_CONFIG,
|
||||||
|
CLIENT_OPTIONS,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
createWebClientResponse,
|
||||||
|
} from '@app/api';
|
||||||
|
|
||||||
export const WebClientContext = createContext<WebClient | null>(null);
|
export const WebClientContext = createContext<WebClient | null>(null);
|
||||||
WebClientContext.displayName = 'WebClientContext';
|
WebClientContext.displayName = 'WebClientContext';
|
||||||
|
|
||||||
export function WebClientProvider({ children }: { children: ReactNode }) {
|
export function WebClientProvider({ children }: { children: ReactNode }) {
|
||||||
const [client] = useState(() => new WebClient(createWebClientRequest(), createWebClientResponse()));
|
const [client] = useState(() => new WebClient(
|
||||||
|
createWebClientResponse(),
|
||||||
|
CLIENT_CONFIG,
|
||||||
|
CLIENT_OPTIONS,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
));
|
||||||
|
|
||||||
return <WebClientContext value={client}>{children}</WebClientContext>;
|
return <WebClientContext value={client}>{children}</WebClientContext>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,312 +0,0 @@
|
||||||
const captured = vi.hoisted(() => ({
|
|
||||||
wsOptions: null as WebSocketServiceConfig | null,
|
|
||||||
pbOptions: null as SocketTransport | null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./services/WebSocketService', () => ({
|
|
||||||
WebSocketService: vi.fn().mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) {
|
|
||||||
captured.wsOptions = options;
|
|
||||||
return {
|
|
||||||
message$: { subscribe: vi.fn() },
|
|
||||||
connect: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
checkReadyState: vi.fn().mockReturnValue(true),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./services/ProtobufService', () => ({
|
|
||||||
ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) {
|
|
||||||
captured.pbOptions = transport;
|
|
||||||
return {
|
|
||||||
handleMessageEvent: vi.fn(),
|
|
||||||
resetCommands: vi.fn(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { WebClient } from './WebClient';
|
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
|
||||||
import { StatusEnum } from './types/StatusEnum';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { Mock } from 'vitest';
|
|
||||||
import { SocketTransport } from './services/ProtobufService';
|
|
||||||
import { WebSocketServiceConfig } from './services/WebSocketService';
|
|
||||||
import type { IWebClientResponse } from './types/WebClientResponse';
|
|
||||||
import type { IWebClientRequest } from './types/WebClientRequest';
|
|
||||||
import type { ConnectTarget } from './types/WebClientConfig';
|
|
||||||
import { installMockWebSocket } from './__mocks__/helpers';
|
|
||||||
import { create, setExtension, toBinary } from '@bufbuild/protobuf';
|
|
||||||
import {
|
|
||||||
Event_ServerIdentification_ext,
|
|
||||||
Event_ServerIdentification_ServerOptions,
|
|
||||||
Event_ServerIdentificationSchema,
|
|
||||||
ServerMessageSchema,
|
|
||||||
ServerMessage_MessageType,
|
|
||||||
SessionEventSchema,
|
|
||||||
} from '@app/generated';
|
|
||||||
import { PROTOCOL_VERSION } from './config';
|
|
||||||
|
|
||||||
function buildServerIdentificationMessage({
|
|
||||||
protocolVersion = PROTOCOL_VERSION,
|
|
||||||
serverOptions = 0,
|
|
||||||
}: { protocolVersion?: number; serverOptions?: number } = {}): Uint8Array {
|
|
||||||
const ident = create(Event_ServerIdentificationSchema, {
|
|
||||||
serverName: 'TestServer',
|
|
||||||
serverVersion: '2.8.0',
|
|
||||||
protocolVersion,
|
|
||||||
serverOptions,
|
|
||||||
});
|
|
||||||
const sessionEvent = create(SessionEventSchema);
|
|
||||||
setExtension(sessionEvent, Event_ServerIdentification_ext, ident);
|
|
||||||
const server = create(ServerMessageSchema, {
|
|
||||||
messageType: ServerMessage_MessageType.SESSION_EVENT,
|
|
||||||
sessionEvent,
|
|
||||||
});
|
|
||||||
return toBinary(ServerMessageSchema, server);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeMockResponse(): IWebClientResponse {
|
|
||||||
return {
|
|
||||||
session: {
|
|
||||||
initialized: vi.fn(),
|
|
||||||
connectionAttempted: vi.fn(),
|
|
||||||
connectionFailed: vi.fn(),
|
|
||||||
clearStore: vi.fn(),
|
|
||||||
updateStatus: vi.fn(),
|
|
||||||
testConnectionSuccessful: vi.fn(),
|
|
||||||
testConnectionFailed: vi.fn(),
|
|
||||||
},
|
|
||||||
room: { clearStore: vi.fn() },
|
|
||||||
game: { clearStore: vi.fn() },
|
|
||||||
admin: {},
|
|
||||||
moderator: {},
|
|
||||||
} as unknown as IWebClientResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeMockRequest(): IWebClientRequest {
|
|
||||||
return {} as IWebClientRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('WebClient', () => {
|
|
||||||
let client: WebClient;
|
|
||||||
let mockResponse: IWebClientResponse;
|
|
||||||
let mockRequest: IWebClientRequest;
|
|
||||||
let messageSubject: Subject<MessageEvent>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
|
|
||||||
|
|
||||||
(ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) {
|
|
||||||
captured.pbOptions = transport;
|
|
||||||
return {
|
|
||||||
handleMessageEvent: vi.fn(),
|
|
||||||
resetCommands: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
messageSubject = new Subject<MessageEvent>();
|
|
||||||
(WebSocketService as Mock).mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) {
|
|
||||||
captured.wsOptions = options;
|
|
||||||
return {
|
|
||||||
message$: messageSubject,
|
|
||||||
connect: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
checkReadyState: vi.fn().mockReturnValue(true),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
|
|
||||||
mockResponse = makeMockResponse();
|
|
||||||
mockRequest = makeMockRequest();
|
|
||||||
client = new WebClient(mockRequest, mockResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('stores the request and response on the instance', () => {
|
|
||||||
expect(client.request).toBe(mockRequest);
|
|
||||||
expect(client.response).toBe(mockResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('subscribes socket.message$ to protobuf.handleMessageEvent', () => {
|
|
||||||
const event = { data: new ArrayBuffer(0) } as MessageEvent;
|
|
||||||
messageSubject.next(event);
|
|
||||||
expect(client.protobuf.handleMessageEvent).toHaveBeenCalledWith(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls response.session.initialized', () => {
|
|
||||||
expect(mockResponse.session.initialized).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets WebClient.instance to the constructed instance', () => {
|
|
||||||
expect(WebClient.instance).toBe(client);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when instantiated more than once', () => {
|
|
||||||
expect(() => new WebClient(makeMockRequest(), makeMockResponse())).toThrow(/singleton/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('static instance accessor', () => {
|
|
||||||
it('throws when accessed before construction', () => {
|
|
||||||
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
|
|
||||||
expect(() => WebClient.instance).toThrow(/not been initialized/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('connect', () => {
|
|
||||||
it('calls response.session.connectionAttempted', () => {
|
|
||||||
const target: ConnectTarget = { host: 'h', port: '1' };
|
|
||||||
client.connect(target);
|
|
||||||
expect(mockResponse.session.connectionAttempted).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls socket.connect with target', () => {
|
|
||||||
const target: ConnectTarget = { host: 'h', port: '1' };
|
|
||||||
client.connect(target);
|
|
||||||
expect(client.socket.connect).toHaveBeenCalledWith(target);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('testConnect', () => {
|
|
||||||
let MockWS: ReturnType<typeof installMockWebSocket>['MockWS'];
|
|
||||||
let wsMockInstance: ReturnType<typeof installMockWebSocket>['mockInstance'];
|
|
||||||
let restoreWS: ReturnType<typeof installMockWebSocket>['restore'];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const installed = installMockWebSocket();
|
|
||||||
MockWS = installed.MockWS;
|
|
||||||
wsMockInstance = installed.mockInstance;
|
|
||||||
restoreWS = installed.restore;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
restoreWS();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
const target: ConnectTarget = { host: 'h', port: '1' };
|
|
||||||
|
|
||||||
it('creates a WebSocket with the correct URL', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
expect(MockWS).toHaveBeenCalledWith(expect.stringContaining('://h:1'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('routes path-bearing hosts through the default TLS port (nginx proxy)', () => {
|
|
||||||
client.testConnect({ host: 'server.example.com/servatrice', port: '4748' });
|
|
||||||
expect(MockWS).toHaveBeenCalledWith(expect.stringMatching(/:\/\/server\.example\.com\/servatrice$/));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches testConnectionSuccessful with supportsHashedPassword=true when the bit is set', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
const data = buildServerIdentificationMessage({
|
|
||||||
serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash,
|
|
||||||
});
|
|
||||||
wsMockInstance.onmessage({ data: data.buffer });
|
|
||||||
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(true);
|
|
||||||
expect(wsMockInstance.close).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches testConnectionSuccessful with supportsHashedPassword=false for naked-password servers', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
const data = buildServerIdentificationMessage({ serverOptions: 0 });
|
|
||||||
wsMockInstance.onmessage({ data: data.buffer });
|
|
||||||
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails on protocol-version mismatch instead of reporting success', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
const data = buildServerIdentificationMessage({ protocolVersion: PROTOCOL_VERSION + 1 });
|
|
||||||
wsMockInstance.onmessage({ data: data.buffer });
|
|
||||||
expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled();
|
|
||||||
expect(mockResponse.session.testConnectionSuccessful).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls testConnectionFailed on error', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
wsMockInstance.onerror();
|
|
||||||
expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fires testConnectionFailed when ServerIdentification never arrives before the keepalive timeout', () => {
|
|
||||||
client.testConnect(target);
|
|
||||||
vi.advanceTimersByTime(5000);
|
|
||||||
expect(wsMockInstance.close).toHaveBeenCalled();
|
|
||||||
expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closes the prior in-flight socket on rapid re-click', () => {
|
|
||||||
const { instances } = installMockWebSocket();
|
|
||||||
// The fresh installMockWebSocket replaces the stub from beforeEach so
|
|
||||||
// we observe the next two constructions in isolation.
|
|
||||||
client.testConnect(target);
|
|
||||||
const first = instances[instances.length - 1];
|
|
||||||
expect(first.close).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
client.testConnect(target);
|
|
||||||
expect(first.close).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('disconnect', () => {
|
|
||||||
it('delegates to socket.disconnect', () => {
|
|
||||||
client.disconnect();
|
|
||||||
expect(client.socket.disconnect).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateStatus', () => {
|
|
||||||
it('sets the status', () => {
|
|
||||||
client.updateStatus(StatusEnum.CONNECTED);
|
|
||||||
expect(client.status).toBe(StatusEnum.CONNECTED);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls protobuf.resetCommands on DISCONNECTED', () => {
|
|
||||||
client.updateStatus(StatusEnum.DISCONNECTED);
|
|
||||||
expect(client.protobuf.resetCommands).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not reset protobuf when status is not DISCONNECTED', () => {
|
|
||||||
client.updateStatus(StatusEnum.CONNECTED);
|
|
||||||
expect(client.protobuf.resetCommands).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor closures', () => {
|
|
||||||
it('keepAliveFn is set to ping function in WebSocketService', () => {
|
|
||||||
expect(captured.wsOptions!.keepAliveFn).toBeDefined();
|
|
||||||
expect(typeof captured.wsOptions!.keepAliveFn).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onStatusChange routes to response.session.updateStatus and updates own status', () => {
|
|
||||||
captured.wsOptions!.onStatusChange(StatusEnum.CONNECTED, 'Connected');
|
|
||||||
expect(mockResponse.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected');
|
|
||||||
expect(client.status).toBe(StatusEnum.CONNECTED);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onConnectionFailed routes to response.session.connectionFailed', () => {
|
|
||||||
captured.wsOptions!.onConnectionFailed();
|
|
||||||
expect(mockResponse.session.connectionFailed).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('send closure delegates to socket.send', () => {
|
|
||||||
const data = new Uint8Array([1, 2, 3]);
|
|
||||||
captured.pbOptions!.send(data);
|
|
||||||
expect(client.socket.send).toHaveBeenCalledWith(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isOpen closure delegates to socket.checkReadyState', () => {
|
|
||||||
const result = captured.pbOptions!.isOpen();
|
|
||||||
expect(client.socket.checkReadyState).toHaveBeenCalledWith(WebSocket.OPEN);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
import { fromBinary, getExtension, hasExtension } from '@bufbuild/protobuf';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Event_ServerIdentification_ext,
|
|
||||||
ServerMessageSchema,
|
|
||||||
ServerMessage_MessageType,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
import { ping } from './commands/session';
|
|
||||||
import { CLIENT_OPTIONS, PROTOCOL_VERSION } from './config';
|
|
||||||
import { GameEvents } from './events/game';
|
|
||||||
import { RoomEvents } from './events/room';
|
|
||||||
import { SessionEvents } from './events/session';
|
|
||||||
import type { ConnectTarget } from './types/WebClientConfig';
|
|
||||||
import type { IWebClientRequest } from './types/WebClientRequest';
|
|
||||||
import type { IWebClientResponse } from './types/WebClientResponse';
|
|
||||||
import { StatusEnum } from './types/StatusEnum';
|
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
|
||||||
import { buildWebSocketUrl } from './utils/buildWebSocketUrl';
|
|
||||||
import { passwordSaltSupported } from './utils/passwordHasher';
|
|
||||||
|
|
||||||
export class WebClient {
|
|
||||||
private static _instance: WebClient | null = null;
|
|
||||||
|
|
||||||
static get instance(): WebClient {
|
|
||||||
if (!WebClient._instance) {
|
|
||||||
throw new Error(
|
|
||||||
'WebClient has not been initialized. Instantiate it via `new WebClient()` before accessing `WebClient.instance`.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return WebClient._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
protobuf: ProtobufService;
|
|
||||||
socket: WebSocketService;
|
|
||||||
status: StatusEnum;
|
|
||||||
private testSocket: WebSocket | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public request: IWebClientRequest,
|
|
||||||
public response: IWebClientResponse
|
|
||||||
) {
|
|
||||||
if (WebClient._instance) {
|
|
||||||
throw new Error('WebClient is a singleton and has already been initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socket = new WebSocketService({
|
|
||||||
keepAliveFn: ping,
|
|
||||||
onStatusChange: (status, description) => {
|
|
||||||
this.response.session.updateStatus(status, description);
|
|
||||||
this.updateStatus(status);
|
|
||||||
},
|
|
||||||
onConnectionFailed: () => {
|
|
||||||
this.response.session.connectionFailed();
|
|
||||||
},
|
|
||||||
reconnect: {
|
|
||||||
maxAttempts: 5,
|
|
||||||
baseDelayMs: 1000,
|
|
||||||
maxDelayMs: 30000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.protobuf = new ProtobufService(
|
|
||||||
{
|
|
||||||
send: (data) => this.socket.send(data),
|
|
||||||
isOpen: () => this.socket.checkReadyState(WebSocket.OPEN),
|
|
||||||
},
|
|
||||||
{ game: GameEvents, room: RoomEvents, session: SessionEvents },
|
|
||||||
);
|
|
||||||
|
|
||||||
this.socket.message$.subscribe((message: MessageEvent) => {
|
|
||||||
this.protobuf.handleMessageEvent(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
WebClient._instance = this;
|
|
||||||
|
|
||||||
this.response.session.initialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect(target: ConnectTarget): void {
|
|
||||||
this.response.session.connectionAttempted();
|
|
||||||
this.socket.connect(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public testConnect(target: ConnectTarget): void {
|
|
||||||
// A prior test connection still in flight when the user re-clicks would
|
|
||||||
// otherwise leak the socket until its keepalive timeout. Close eagerly.
|
|
||||||
if (this.testSocket) {
|
|
||||||
this.testSocket.close();
|
|
||||||
this.testSocket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = window.location.hostname === 'localhost' ? 'ws' : 'wss';
|
|
||||||
const socket = new WebSocket(buildWebSocketUrl(protocol, target.host, target.port));
|
|
||||||
socket.binaryType = 'arraybuffer';
|
|
||||||
this.testSocket = socket;
|
|
||||||
|
|
||||||
// "Green" means reachable AND speaking a compatible Cockatrice protocol.
|
|
||||||
// Waiting for Event_ServerIdentification lets us read the hashed-password
|
|
||||||
// capability before the user ever logs in. The bitmask is resolved here
|
|
||||||
// (the websocket layer owns protocol details) so downstream consumers
|
|
||||||
// receive a domain-level boolean instead of a raw integer.
|
|
||||||
let resolved = false;
|
|
||||||
const resolve = (ok: boolean, supportsHashedPassword = false): void => {
|
|
||||||
if (resolved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolved = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
// Suppress dispatches from a superseded socket — a newer test has
|
|
||||||
// already taken over and we'd race a stale result into its pending-ref.
|
|
||||||
if (this.testSocket === socket) {
|
|
||||||
if (ok) {
|
|
||||||
this.response.session.testConnectionSuccessful(supportsHashedPassword);
|
|
||||||
} else {
|
|
||||||
this.response.session.testConnectionFailed();
|
|
||||||
}
|
|
||||||
this.testSocket = null;
|
|
||||||
}
|
|
||||||
socket.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => resolve(false), CLIENT_OPTIONS.keepalive);
|
|
||||||
|
|
||||||
socket.onmessage = (event: MessageEvent) => {
|
|
||||||
try {
|
|
||||||
const msg = fromBinary(ServerMessageSchema, new Uint8Array(event.data));
|
|
||||||
if (msg.messageType !== ServerMessage_MessageType.SESSION_EVENT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sessionEvent = msg.sessionEvent;
|
|
||||||
if (!sessionEvent || !hasExtension(sessionEvent, Event_ServerIdentification_ext)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ident = getExtension(sessionEvent, Event_ServerIdentification_ext);
|
|
||||||
if (ident.protocolVersion !== PROTOCOL_VERSION) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(true, passwordSaltSupported(ident.serverOptions));
|
|
||||||
} catch {
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = () => resolve(false);
|
|
||||||
socket.onclose = () => resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnect(): void {
|
|
||||||
this.socket.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateStatus(status: StatusEnum): void {
|
|
||||||
this.status = status;
|
|
||||||
|
|
||||||
if (status === StatusEnum.DISCONNECTED) {
|
|
||||||
this.protobuf.resetCommands();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isReconnecting(): boolean {
|
|
||||||
return this.status === StatusEnum.RECONNECTING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
/**
|
|
||||||
* Shared WebClient mock — the single source of truth for all websocket
|
|
||||||
* layer unit tests.
|
|
||||||
*
|
|
||||||
* Vitest resolves this file whenever a spec calls `vi.mock('...WebClient')`
|
|
||||||
* without providing a factory. Each spec file gets its own module graph
|
|
||||||
* (isolate: true), so there are no factory-conflict issues.
|
|
||||||
*
|
|
||||||
* Usage in spec files:
|
|
||||||
*
|
|
||||||
* vi.mock('../../WebClient');
|
|
||||||
* import { WebClient } from '../../WebClient';
|
|
||||||
* // WebClient.instance.response.game.cardMoved ← vi.fn()
|
|
||||||
* // WebClient.instance.protobuf.sendGameCommand ← vi.fn()
|
|
||||||
*
|
|
||||||
* `useWebClientCleanup()` is NOT required — `instance` is a plain
|
|
||||||
* property, not a getter that throws.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const session = {
|
|
||||||
initialized: vi.fn(),
|
|
||||||
connectionAttempted: vi.fn(),
|
|
||||||
clearStore: vi.fn(),
|
|
||||||
loginSuccessful: vi.fn(),
|
|
||||||
loginFailed: vi.fn(),
|
|
||||||
connectionFailed: vi.fn(),
|
|
||||||
testConnectionSuccessful: vi.fn(),
|
|
||||||
testConnectionFailed: vi.fn(),
|
|
||||||
updateBuddyList: vi.fn(),
|
|
||||||
addToBuddyList: vi.fn(),
|
|
||||||
removeFromBuddyList: vi.fn(),
|
|
||||||
updateIgnoreList: vi.fn(),
|
|
||||||
addToIgnoreList: vi.fn(),
|
|
||||||
removeFromIgnoreList: vi.fn(),
|
|
||||||
updateInfo: vi.fn(),
|
|
||||||
updateStatus: vi.fn(),
|
|
||||||
updateUser: vi.fn(),
|
|
||||||
updateUsers: vi.fn(),
|
|
||||||
userJoined: vi.fn(),
|
|
||||||
userLeft: vi.fn(),
|
|
||||||
serverMessage: vi.fn(),
|
|
||||||
accountAwaitingActivation: vi.fn(),
|
|
||||||
accountActivationSuccess: vi.fn(),
|
|
||||||
accountActivationFailed: vi.fn(),
|
|
||||||
registrationRequiresEmail: vi.fn(),
|
|
||||||
registrationSuccess: vi.fn(),
|
|
||||||
registrationFailed: vi.fn(),
|
|
||||||
registrationEmailError: vi.fn(),
|
|
||||||
registrationPasswordError: vi.fn(),
|
|
||||||
registrationUserNameError: vi.fn(),
|
|
||||||
resetPasswordChallenge: vi.fn(),
|
|
||||||
resetPassword: vi.fn(),
|
|
||||||
resetPasswordSuccess: vi.fn(),
|
|
||||||
resetPasswordFailed: vi.fn(),
|
|
||||||
accountPasswordChange: vi.fn(),
|
|
||||||
accountEditChanged: vi.fn(),
|
|
||||||
accountImageChanged: vi.fn(),
|
|
||||||
getUserInfo: vi.fn(),
|
|
||||||
getGamesOfUser: vi.fn(),
|
|
||||||
gameJoined: vi.fn(),
|
|
||||||
notifyUser: vi.fn(),
|
|
||||||
playerPropertiesChanged: vi.fn(),
|
|
||||||
serverShutdown: vi.fn(),
|
|
||||||
userMessage: vi.fn(),
|
|
||||||
addToList: vi.fn(),
|
|
||||||
removeFromList: vi.fn(),
|
|
||||||
deleteServerDeck: vi.fn(),
|
|
||||||
updateServerDecks: vi.fn(),
|
|
||||||
uploadServerDeck: vi.fn(),
|
|
||||||
downloadServerDeck: vi.fn(),
|
|
||||||
createServerDeckDir: vi.fn(),
|
|
||||||
deleteServerDeckDir: vi.fn(),
|
|
||||||
replayList: vi.fn(),
|
|
||||||
replayAdded: vi.fn(),
|
|
||||||
replayModifyMatch: vi.fn(),
|
|
||||||
replayDeleteMatch: vi.fn(),
|
|
||||||
replayDownloaded: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const room = {
|
|
||||||
clearStore: vi.fn(),
|
|
||||||
joinRoom: vi.fn(),
|
|
||||||
leaveRoom: vi.fn(),
|
|
||||||
updateRooms: vi.fn(),
|
|
||||||
updateGames: vi.fn(),
|
|
||||||
addMessage: vi.fn(),
|
|
||||||
userJoined: vi.fn(),
|
|
||||||
userLeft: vi.fn(),
|
|
||||||
removeMessages: vi.fn(),
|
|
||||||
gameCreated: vi.fn(),
|
|
||||||
joinedGame: vi.fn(),
|
|
||||||
setJoinGamePending: vi.fn(),
|
|
||||||
setJoinGameError: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const game = {
|
|
||||||
clearStore: vi.fn(),
|
|
||||||
gameStateChanged: vi.fn(),
|
|
||||||
playerJoined: vi.fn(),
|
|
||||||
playerLeft: vi.fn(),
|
|
||||||
playerPropertiesChanged: vi.fn(),
|
|
||||||
gameClosed: vi.fn(),
|
|
||||||
gameHostChanged: vi.fn(),
|
|
||||||
kicked: vi.fn(),
|
|
||||||
gameSay: vi.fn(),
|
|
||||||
cardMoved: vi.fn(),
|
|
||||||
cardFlipped: vi.fn(),
|
|
||||||
cardDestroyed: vi.fn(),
|
|
||||||
cardAttached: vi.fn(),
|
|
||||||
tokenCreated: vi.fn(),
|
|
||||||
cardAttrChanged: vi.fn(),
|
|
||||||
cardCounterChanged: vi.fn(),
|
|
||||||
arrowCreated: vi.fn(),
|
|
||||||
arrowDeleted: vi.fn(),
|
|
||||||
counterCreated: vi.fn(),
|
|
||||||
counterSet: vi.fn(),
|
|
||||||
counterDeleted: vi.fn(),
|
|
||||||
cardsDrawn: vi.fn(),
|
|
||||||
cardsRevealed: vi.fn(),
|
|
||||||
zoneShuffled: vi.fn(),
|
|
||||||
dieRolled: vi.fn(),
|
|
||||||
activePlayerSet: vi.fn(),
|
|
||||||
activePhaseSet: vi.fn(),
|
|
||||||
turnReversed: vi.fn(),
|
|
||||||
zoneDumped: vi.fn(),
|
|
||||||
zonePropertiesChanged: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const admin = {
|
|
||||||
adjustMod: vi.fn(),
|
|
||||||
reloadConfig: vi.fn(),
|
|
||||||
shutdownServer: vi.fn(),
|
|
||||||
updateServerMessage: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const moderator = {
|
|
||||||
banFromServer: vi.fn(),
|
|
||||||
banHistory: vi.fn(),
|
|
||||||
viewLogs: vi.fn(),
|
|
||||||
warnHistory: vi.fn(),
|
|
||||||
warnListOptions: vi.fn(),
|
|
||||||
warnUser: vi.fn(),
|
|
||||||
grantReplayAccess: vi.fn(),
|
|
||||||
forceActivateUser: vi.fn(),
|
|
||||||
getAdminNotes: vi.fn(),
|
|
||||||
updateAdminNotes: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebClient = {
|
|
||||||
_instance: null as any,
|
|
||||||
instance: {
|
|
||||||
connect: vi.fn(),
|
|
||||||
testConnect: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
updateStatus: vi.fn(),
|
|
||||||
status: 0 as number,
|
|
||||||
config: {},
|
|
||||||
protobuf: {
|
|
||||||
sendSessionCommand: vi.fn(),
|
|
||||||
sendRoomCommand: vi.fn(),
|
|
||||||
sendGameCommand: vi.fn(),
|
|
||||||
sendAdminCommand: vi.fn(),
|
|
||||||
sendModeratorCommand: vi.fn(),
|
|
||||||
resetCommands: vi.fn(),
|
|
||||||
},
|
|
||||||
response: { session, room, game, admin, moderator },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/**
|
|
||||||
* Factory for invoking BackendService command callbacks in unit tests.
|
|
||||||
*
|
|
||||||
* @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 (ext, value, options).
|
|
||||||
* Use 3 for sendRoomCommand (roomId, ext, value, options).
|
|
||||||
*/
|
|
||||||
import { Mock } from 'vitest';
|
|
||||||
|
|
||||||
export function makeCallbackHelpers(mockFn: Mock, optsArgIndex = 2) {
|
|
||||||
function getLastSendOpts() {
|
|
||||||
const calls = mockFn.mock.calls;
|
|
||||||
return calls[calls.length - 1]?.[optsArgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeOnSuccess(response: any = {}, raw?: any) {
|
|
||||||
getLastSendOpts()?.onSuccess?.(response, raw ?? response);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeResponseCode(code: number, raw: any = { responseCode: code }) {
|
|
||||||
const opts = getLastSendOpts();
|
|
||||||
if (opts?.onResponseCode?.[code]) {
|
|
||||||
opts.onResponseCode[code](raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeOnError(code: number = 99, raw: any = {}) {
|
|
||||||
getLastSendOpts()?.onError?.(code, raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeCallback(callbackName: string, ...args: any[]) {
|
|
||||||
const opts = getLastSendOpts();
|
|
||||||
if (opts?.[callbackName]) {
|
|
||||||
opts[callbackName](...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError, invokeCallback };
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/**
|
|
||||||
* Shared mock factories for websocket layer unit tests.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Builds a mock WebSocket instance */
|
|
||||||
export function makeMockWebSocketInstance() {
|
|
||||||
return {
|
|
||||||
send: vi.fn(),
|
|
||||||
close: vi.fn(),
|
|
||||||
readyState: WebSocket.OPEN as number,
|
|
||||||
binaryType: '' as BinaryType,
|
|
||||||
onopen: null as any,
|
|
||||||
onclose: null as any,
|
|
||||||
onerror: null as any,
|
|
||||||
onmessage: null as any,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Installs a mock WebSocket constructor on global. Returns the mock instance and a cleanup function. */
|
|
||||||
export function installMockWebSocket() {
|
|
||||||
const originalWebSocket = (globalThis as any).WebSocket;
|
|
||||||
const mockInstance = makeMockWebSocketInstance();
|
|
||||||
const instances: ReturnType<typeof makeMockWebSocketInstance>[] = [mockInstance];
|
|
||||||
let firstCall = true;
|
|
||||||
const MockWS = vi.fn(function MockWebSocket() {
|
|
||||||
if (firstCall) {
|
|
||||||
firstCall = false;
|
|
||||||
return mockInstance;
|
|
||||||
}
|
|
||||||
const next = makeMockWebSocketInstance();
|
|
||||||
instances.push(next);
|
|
||||||
return next;
|
|
||||||
}) as any;
|
|
||||||
MockWS.OPEN = 1;
|
|
||||||
MockWS.CLOSED = 3;
|
|
||||||
(globalThis as any).WebSocket = MockWS;
|
|
||||||
return { MockWS, mockInstance, instances, restore: () => {
|
|
||||||
(globalThis as any).WebSocket = originalWebSocket;
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
/**
|
|
||||||
* Shared mock shape factories for session command specs.
|
|
||||||
*
|
|
||||||
* Usage inside vi.mock() factory callbacks:
|
|
||||||
*
|
|
||||||
* vi.mock('../../WebClient', async () => {
|
|
||||||
* const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
|
|
||||||
* return { WebClient: { instance: makeWebClientMock() } };
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Superset WebClient instance mock — covers all properties used across both session spec files. */
|
|
||||||
export function makeWebClientMock() {
|
|
||||||
return {
|
|
||||||
connect: vi.fn(),
|
|
||||||
testConnect: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
updateStatus: vi.fn(),
|
|
||||||
config: {},
|
|
||||||
status: 0,
|
|
||||||
protobuf: {
|
|
||||||
sendSessionCommand: vi.fn(),
|
|
||||||
sendRoomCommand: vi.fn(),
|
|
||||||
sendGameCommand: vi.fn(),
|
|
||||||
sendAdminCommand: vi.fn(),
|
|
||||||
sendModeratorCommand: vi.fn(),
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
session: makeSessionPersistenceMock(),
|
|
||||||
room: { joinRoom: vi.fn() },
|
|
||||||
game: {},
|
|
||||||
admin: {},
|
|
||||||
moderator: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Utils mock with unified return values. */
|
|
||||||
export function makeUtilsMock() {
|
|
||||||
return {
|
|
||||||
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: 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(),
|
|
||||||
downloadServerDeck: vi.fn(),
|
|
||||||
replayDownloaded: 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 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: 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,14 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_AdjustMod_ext, Command_AdjustModSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void {
|
|
||||||
WebClient.instance.protobuf.sendAdminCommand(
|
|
||||||
Command_AdjustMod_ext,
|
|
||||||
create(Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.admin.adjustMod(userName, shouldBeMod, shouldBeJudge);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
vi.mock('../../WebClient');
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import { adjustMod } from './adjustMod';
|
|
||||||
import { reloadConfig } from './reloadConfig';
|
|
||||||
import { shutdownServer } from './shutdownServer';
|
|
||||||
import { updateServerMessage } from './updateServerMessage';
|
|
||||||
import {
|
|
||||||
Command_AdjustMod_ext,
|
|
||||||
Command_ReloadConfig_ext,
|
|
||||||
Command_ShutdownServer_ext,
|
|
||||||
Command_UpdateServerMessage_ext,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
import { Mock } from 'vitest';
|
|
||||||
|
|
||||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
|
||||||
WebClient.instance.protobuf.sendAdminCommand as Mock,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('adjustMod', () => {
|
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => {
|
|
||||||
adjustMod('alice', true, false);
|
|
||||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
|
||||||
Command_AdjustMod_ext,
|
|
||||||
expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.admin.adjustMod', () => {
|
|
||||||
adjustMod('alice', true, false);
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.admin.adjustMod).toHaveBeenCalledWith('alice', true, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('reloadConfig', () => {
|
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_ReloadConfig extension', () => {
|
|
||||||
reloadConfig();
|
|
||||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
|
||||||
Command_ReloadConfig_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.admin.reloadConfig', () => {
|
|
||||||
reloadConfig();
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.admin.reloadConfig).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('shutdownServer', () => {
|
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => {
|
|
||||||
shutdownServer('maintenance', 10);
|
|
||||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
|
||||||
Command_ShutdownServer_ext,
|
|
||||||
expect.objectContaining({ reason: 'maintenance', minutes: 10 }),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.admin.shutdownServer', () => {
|
|
||||||
shutdownServer('maintenance', 10);
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.admin.shutdownServer).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateServerMessage', () => {
|
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => {
|
|
||||||
updateServerMessage();
|
|
||||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
|
||||||
Command_UpdateServerMessage_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.admin.updateServerMessage', () => {
|
|
||||||
updateServerMessage();
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.admin.updateServerMessage).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './adjustMod';
|
|
||||||
export * from './reloadConfig';
|
|
||||||
export * from './shutdownServer';
|
|
||||||
export * from './updateServerMessage';
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
export function reloadConfig(): void {
|
|
||||||
WebClient.instance.protobuf.sendAdminCommand(Command_ReloadConfig_ext, create(Command_ReloadConfigSchema), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.admin.reloadConfig();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_ShutdownServer_ext, Command_ShutdownServerSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
export function shutdownServer(reason: string, minutes: number): void {
|
|
||||||
WebClient.instance.protobuf.sendAdminCommand(
|
|
||||||
Command_ShutdownServer_ext,
|
|
||||||
create(Command_ShutdownServerSchema, { reason, minutes }),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.admin.shutdownServer();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
export function updateServerMessage(): void {
|
|
||||||
WebClient.instance.protobuf.sendAdminCommand(Command_UpdateServerMessage_ext, create(Command_UpdateServerMessageSchema), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.admin.updateServerMessage();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// Byte-level verification that Command_AttachCard's proto2 optional target
|
|
||||||
// fields stay UNSET when we build an "Unattach" — i.e. the client omits
|
|
||||||
// them entirely rather than sending sentinel values. Desktop's server-side
|
|
||||||
// `has_target_player_id()` / `has_target_zone()` / `has_target_card_id()`
|
|
||||||
// drives the unattach branch; if any target field gets serialized, the
|
|
||||||
// server treats it as a re-attach instead of an unattach.
|
|
||||||
//
|
|
||||||
// See the M6 Unattach deferrable in webclient/plans/gameboard-deferrables.md.
|
|
||||||
|
|
||||||
import { create, isFieldSet, toBinary, fromBinary } from '@bufbuild/protobuf';
|
|
||||||
import { Command_AttachCardSchema } from '@app/generated';
|
|
||||||
|
|
||||||
describe('Command_AttachCard proto2 presence (Unattach invariant)', () => {
|
|
||||||
it('omitting target fields leaves them unset on the in-memory message', () => {
|
|
||||||
const msg = create(Command_AttachCardSchema, {
|
|
||||||
startZone: 'table',
|
|
||||||
cardId: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.startZone)).toBe(true);
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.cardId)).toBe(true);
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.targetPlayerId)).toBe(false);
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.targetZone)).toBe(false);
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.targetCardId)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('omitting target fields omits them from the serialized bytes', () => {
|
|
||||||
const msg = create(Command_AttachCardSchema, {
|
|
||||||
startZone: 'table',
|
|
||||||
cardId: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bytes = toBinary(Command_AttachCardSchema, msg);
|
|
||||||
// Round-trip to confirm the wire format also reports the fields unset.
|
|
||||||
const parsed = fromBinary(Command_AttachCardSchema, bytes);
|
|
||||||
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetPlayerId)).toBe(false);
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetZone)).toBe(false);
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetCardId)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passing an explicit targetPlayerId DOES set presence (so "attach" paths still work)', () => {
|
|
||||||
const msg = create(Command_AttachCardSchema, {
|
|
||||||
startZone: 'table',
|
|
||||||
cardId: 10,
|
|
||||||
targetPlayerId: 5,
|
|
||||||
targetZone: 'table',
|
|
||||||
targetCardId: 99,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bytes = toBinary(Command_AttachCardSchema, msg);
|
|
||||||
const parsed = fromBinary(Command_AttachCardSchema, bytes);
|
|
||||||
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetPlayerId)).toBe(true);
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetZone)).toBe(true);
|
|
||||||
expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetCardId)).toBe(true);
|
|
||||||
expect(parsed.targetPlayerId).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setting a target field to its default -1 still records presence (presence != default)', () => {
|
|
||||||
// This is the trap the deferrable warned about: proto2 `optional` with a
|
|
||||||
// default of -1 treats an explicit `-1` as "field set with value -1",
|
|
||||||
// which the server interprets as "attach to slot -1" rather than unattach.
|
|
||||||
// Clients must OMIT the field entirely to unattach.
|
|
||||||
const msg = create(Command_AttachCardSchema, {
|
|
||||||
startZone: 'table',
|
|
||||||
cardId: 10,
|
|
||||||
targetPlayerId: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(isFieldSet(msg, Command_AttachCardSchema.field.targetPlayerId)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_AttachCard_ext, Command_AttachCardSchema, type AttachCardParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function attachCard(gameId: number, params: AttachCardParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_ChangeZoneProperties_ext, Command_ChangeZonePropertiesSchema, type ChangeZonePropertiesParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(
|
|
||||||
gameId,
|
|
||||||
Command_ChangeZoneProperties_ext,
|
|
||||||
create(Command_ChangeZonePropertiesSchema, params)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_Concede_ext, Command_ConcedeSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function concede(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_Concede_ext, create(Command_ConcedeSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_CreateArrow_ext, Command_CreateArrowSchema, type CreateArrowParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function createArrow(gameId: number, params: CreateArrowParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_CreateCounter_ext, Command_CreateCounterSchema, type CreateCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function createCounter(gameId: number, params: CreateCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_CreateToken_ext, Command_CreateTokenSchema, type CreateTokenParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function createToken(gameId: number, params: CreateTokenParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_DeckSelect_ext, Command_DeckSelectSchema, type DeckSelectParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function deckSelect(gameId: number, params: DeckSelectParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_DelCounter_ext, Command_DelCounterSchema, type DelCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function delCounter(gameId: number, params: DelCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_DeleteArrow_ext, Command_DeleteArrowSchema, type DeleteArrowParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function deleteArrow(gameId: number, params: DeleteArrowParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_DrawCards_ext, Command_DrawCardsSchema, type DrawCardsParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function drawCards(gameId: number, params: DrawCardsParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_DumpZone_ext, Command_DumpZoneSchema, type DumpZoneParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function dumpZone(gameId: number, params: DumpZoneParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_FlipCard_ext, Command_FlipCardSchema, type FlipCardParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function flipCard(gameId: number, params: FlipCardParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
vi.mock('../../WebClient');
|
|
||||||
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import { create, setExtension } from '@bufbuild/protobuf';
|
|
||||||
import {
|
|
||||||
Command_AttachCard_ext,
|
|
||||||
Command_ChangeZoneProperties_ext,
|
|
||||||
Command_Concede_ext,
|
|
||||||
Command_CreateArrow_ext,
|
|
||||||
Command_CreateCounter_ext,
|
|
||||||
Command_CreateToken_ext,
|
|
||||||
Command_DeckSelect_ext,
|
|
||||||
Command_DelCounter_ext,
|
|
||||||
Command_DeleteArrow_ext,
|
|
||||||
Command_DrawCards_ext,
|
|
||||||
Command_DrawCardsSchema,
|
|
||||||
Command_DumpZone_ext,
|
|
||||||
Command_FlipCard_ext,
|
|
||||||
Command_GameSay_ext,
|
|
||||||
Command_IncCardCounter_ext,
|
|
||||||
Command_IncCounter_ext,
|
|
||||||
Command_Judge_ext,
|
|
||||||
Command_KickFromGame_ext,
|
|
||||||
Command_LeaveGame_ext,
|
|
||||||
Command_MoveCard_ext,
|
|
||||||
Command_Mulligan_ext,
|
|
||||||
Command_NextTurn_ext,
|
|
||||||
Command_ReadyStart_ext,
|
|
||||||
Command_RevealCards_ext,
|
|
||||||
Command_ReverseTurn_ext,
|
|
||||||
Command_RollDie_ext,
|
|
||||||
Command_SetActivePhase_ext,
|
|
||||||
Command_SetCardAttr_ext,
|
|
||||||
Command_SetCardCounter_ext,
|
|
||||||
Command_SetCounter_ext,
|
|
||||||
Command_SetSideboardLock_ext,
|
|
||||||
Command_SetSideboardPlan_ext,
|
|
||||||
Command_Shuffle_ext,
|
|
||||||
Command_UndoDraw_ext,
|
|
||||||
Command_Unconcede_ext,
|
|
||||||
GameCommandSchema,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
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 { rollDie } from './rollDie';
|
|
||||||
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';
|
|
||||||
|
|
||||||
const gameId = 1;
|
|
||||||
|
|
||||||
describe('Game commands — delegate to WebClient.instance.protobuf.sendGameCommand', () => {
|
|
||||||
it('attachCard sends Command_AttachCard', () => {
|
|
||||||
attachCard(gameId, { cardId: 10, startZone: 'hand' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('changeZoneProperties sends Command_ChangeZoneProperties', () => {
|
|
||||||
changeZoneProperties(gameId, { zoneName: 'side' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('concede sends Command_Concede with empty object', () => {
|
|
||||||
concede(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Concede_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createArrow sends Command_CreateArrow', () => {
|
|
||||||
createArrow(gameId, { startPlayerId: 1, startZone: 'hand' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createCounter sends Command_CreateCounter', () => {
|
|
||||||
createCounter(gameId, { counterName: 'life' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createToken sends Command_CreateToken', () => {
|
|
||||||
createToken(gameId, { cardName: 'Goblin', zone: 'play' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deckSelect sends Command_DeckSelect', () => {
|
|
||||||
deckSelect(gameId, { deckId: 5 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delCounter sends Command_DelCounter', () => {
|
|
||||||
delCounter(gameId, { counterId: 3 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteArrow sends Command_DeleteArrow', () => {
|
|
||||||
deleteArrow(gameId, { arrowId: 2 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('drawCards sends Command_DrawCards', () => {
|
|
||||||
drawCards(gameId, { number: 3 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_DrawCards_ext, expect.objectContaining({ number: 3 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dumpZone sends Command_DumpZone', () => {
|
|
||||||
dumpZone(gameId, { playerId: 2, zoneName: 'library' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('flipCard sends Command_FlipCard', () => {
|
|
||||||
flipCard(gameId, { cardId: 7, faceDown: false });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gameSay sends Command_GameSay', () => {
|
|
||||||
gameSay(gameId, { message: 'hello' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_GameSay_ext, expect.objectContaining({ message: 'hello' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('incCardCounter sends Command_IncCardCounter', () => {
|
|
||||||
incCardCounter(gameId, { cardId: 5, counterId: 1 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('incCounter sends Command_IncCounter', () => {
|
|
||||||
incCounter(gameId, { counterId: 1, delta: 5 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('kickFromGame sends Command_KickFromGame', () => {
|
|
||||||
kickFromGame(gameId, { playerId: 2 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('leaveGame sends Command_LeaveGame with empty object', () => {
|
|
||||||
leaveGame(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_LeaveGame_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('moveCard sends Command_MoveCard', () => {
|
|
||||||
moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_MoveCard_ext,
|
|
||||||
expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('mulligan sends Command_Mulligan', () => {
|
|
||||||
mulligan(gameId, { number: 7 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_Mulligan_ext, expect.objectContaining({ number: 7 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('nextTurn sends Command_NextTurn with empty object', () => {
|
|
||||||
nextTurn(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_NextTurn_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('readyStart sends Command_ReadyStart', () => {
|
|
||||||
readyStart(gameId, { ready: true });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_ReadyStart_ext, expect.objectContaining({ ready: true })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('revealCards sends Command_RevealCards', () => {
|
|
||||||
revealCards(gameId, { zoneName: 'hand', cardId: [1, 2] });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reverseTurn sends Command_ReverseTurn with empty object', () => {
|
|
||||||
reverseTurn(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReverseTurn_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setActivePhase sends Command_SetActivePhase', () => {
|
|
||||||
setActivePhase(gameId, { phase: 2 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setCardAttr sends Command_SetCardAttr', () => {
|
|
||||||
setCardAttr(gameId, { zone: 'play', cardId: 5, attrValue: '2' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetCardAttr_ext,
|
|
||||||
expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setCardCounter sends Command_SetCardCounter', () => {
|
|
||||||
setCardCounter(gameId, { cardId: 5, counterId: 1 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setCounter sends Command_SetCounter', () => {
|
|
||||||
setCounter(gameId, { counterId: 1, value: 10 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setSideboardLock sends Command_SetSideboardLock', () => {
|
|
||||||
setSideboardLock(gameId, { locked: true });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setSideboardPlan sends Command_SetSideboardPlan', () => {
|
|
||||||
setSideboardPlan(gameId, { moveList: [] });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shuffle sends Command_Shuffle', () => {
|
|
||||||
shuffle(gameId, { zoneName: 'hand' });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('undoDraw sends Command_UndoDraw with empty object', () => {
|
|
||||||
undoDraw(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_UndoDraw_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unconcede sends Command_Unconcede with empty object', () => {
|
|
||||||
unconcede(gameId);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rollDie sends Command_RollDie', () => {
|
|
||||||
rollDie(gameId, { sides: 6, count: 2 });
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId, Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => {
|
|
||||||
const targetId = 3;
|
|
||||||
const innerCmd = create(GameCommandSchema);
|
|
||||||
setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 }));
|
|
||||||
judge(gameId, targetId, innerCmd);
|
|
||||||
expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(
|
|
||||||
gameId,
|
|
||||||
Command_Judge_ext,
|
|
||||||
expect.objectContaining({ targetId: 3, gameCommand: expect.any(Array) })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_GameSay_ext, Command_GameSaySchema, type GameSayParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function gameSay(gameId: number, params: GameSayParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_IncCardCounter_ext, Command_IncCardCounterSchema, type IncCardCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function incCardCounter(gameId: number, params: IncCardCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_IncCounter_ext, Command_IncCounterSchema, type IncCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function incCounter(gameId: number, params: IncCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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';
|
|
||||||
export { rollDie } from './rollDie';
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_Judge_ext, Command_JudgeSchema, type GameCommand } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_Judge_ext, create(Command_JudgeSchema, {
|
|
||||||
targetId,
|
|
||||||
gameCommand: [innerGameCommand],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_KickFromGame_ext, Command_KickFromGameSchema, type KickFromGameParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function kickFromGame(gameId: number, params: KickFromGameParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_LeaveGame_ext, Command_LeaveGameSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function leaveGame(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_LeaveGame_ext, create(Command_LeaveGameSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_MoveCard_ext, Command_MoveCardSchema, type MoveCardParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function moveCard(gameId: number, params: MoveCardParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_Mulligan_ext, Command_MulliganSchema, type MulliganParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function mulligan(gameId: number, params: MulliganParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_NextTurn_ext, Command_NextTurnSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function nextTurn(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_NextTurn_ext, create(Command_NextTurnSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_ReadyStart_ext, Command_ReadyStartSchema, type ReadyStartParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function readyStart(gameId: number, params: ReadyStartParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_RevealCards_ext, Command_RevealCardsSchema, type RevealCardsParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function revealCards(gameId: number, params: RevealCardsParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_ReverseTurn_ext, Command_ReverseTurnSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function reverseTurn(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReverseTurn_ext, create(Command_ReverseTurnSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_RollDie_ext, Command_RollDieSchema, type RollDieParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function rollDie(gameId: number, params: RollDieParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_RollDie_ext, create(Command_RollDieSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetActivePhase_ext, Command_SetActivePhaseSchema, type SetActivePhaseParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setActivePhase(gameId: number, params: SetActivePhaseParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetCardAttr_ext, Command_SetCardAttrSchema, type SetCardAttrParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setCardAttr(gameId: number, params: SetCardAttrParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetCardCounter_ext, Command_SetCardCounterSchema, type SetCardCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setCardCounter(gameId: number, params: SetCardCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetCounter_ext, Command_SetCounterSchema, type SetCounterParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setCounter(gameId: number, params: SetCounterParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetSideboardLock_ext, Command_SetSideboardLockSchema, type SetSideboardLockParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(
|
|
||||||
gameId,
|
|
||||||
Command_SetSideboardLock_ext,
|
|
||||||
create(Command_SetSideboardLockSchema, params)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_SetSideboardPlan_ext, Command_SetSideboardPlanSchema, type SetSideboardPlanParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(
|
|
||||||
gameId,
|
|
||||||
Command_SetSideboardPlan_ext,
|
|
||||||
create(Command_SetSideboardPlanSchema, params)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_Shuffle_ext, Command_ShuffleSchema, type ShuffleParams } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function shuffle(gameId: number, params: ShuffleParams): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_Unconcede_ext, Command_UnconcedeSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function unconcede(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_Unconcede_ext, create(Command_UnconcedeSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Command_UndoDraw_ext, Command_UndoDrawSchema } from '@app/generated';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
export function undoDraw(gameId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema));
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
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,15 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_BanFromServer_ext, Command_BanFromServerSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string,
|
|
||||||
visibleReason?: string, clientid?: string, removeMessages?: number): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_BanFromServer_ext, create(Command_BanFromServerSchema, {
|
|
||||||
minutes, userName, address, reason, visibleReason, clientid, removeMessages
|
|
||||||
}), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.moderator.banFromServer(userName);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_ForceActivateUser_ext, Command_ForceActivateUserSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function forceActivateUser(usernameToActivate: string, moderatorName: string): void {
|
|
||||||
const cmd = create(Command_ForceActivateUserSchema, { usernameToActivate, moderatorName });
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_ForceActivateUser_ext, cmd, {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.moderator.forceActivateUser(usernameToActivate, moderatorName);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema, Response_GetAdminNotes_ext } from '@app/generated';
|
|
||||||
|
|
||||||
export function getAdminNotes(userName: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_GetAdminNotes_ext, create(Command_GetAdminNotesSchema, { userName }), {
|
|
||||||
responseExt: Response_GetAdminNotes_ext,
|
|
||||||
onSuccess: (response) => {
|
|
||||||
WebClient.instance.response.moderator.getAdminNotes(userName, response.notes);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_GetBanHistory_ext, Command_GetBanHistorySchema, Response_BanHistory_ext } from '@app/generated';
|
|
||||||
|
|
||||||
export function getBanHistory(userName: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_GetBanHistory_ext, create(Command_GetBanHistorySchema, { userName }), {
|
|
||||||
responseExt: Response_BanHistory_ext,
|
|
||||||
onSuccess: (response) => {
|
|
||||||
WebClient.instance.response.moderator.banHistory(userName, response.banList);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema, Response_WarnHistory_ext } from '@app/generated';
|
|
||||||
|
|
||||||
export function getWarnHistory(userName: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(
|
|
||||||
Command_GetWarnHistory_ext,
|
|
||||||
create(Command_GetWarnHistorySchema, { userName }),
|
|
||||||
{
|
|
||||||
responseExt: Response_WarnHistory_ext,
|
|
||||||
onSuccess: (response) => {
|
|
||||||
WebClient.instance.response.moderator.warnHistory(userName, response.warnList);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_GetWarnList_ext, Command_GetWarnListSchema, Response_WarnList_ext } from '@app/generated';
|
|
||||||
|
|
||||||
export function getWarnList(modName: string, userName: string, userClientid: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(
|
|
||||||
Command_GetWarnList_ext,
|
|
||||||
create(Command_GetWarnListSchema, { modName, userName, userClientid }),
|
|
||||||
{
|
|
||||||
responseExt: Response_WarnList_ext,
|
|
||||||
onSuccess: (response) => {
|
|
||||||
WebClient.instance.response.moderator.warnListOptions([response]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function grantReplayAccess(replayId: number, moderatorName: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(
|
|
||||||
Command_GrantReplayAccess_ext,
|
|
||||||
create(Command_GrantReplayAccessSchema, { replayId, moderatorName }),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.moderator.grantReplayAccess(replayId, moderatorName);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export * from './banFromServer';
|
|
||||||
export * from './forceActivateUser';
|
|
||||||
export * from './getAdminNotes';
|
|
||||||
export * from './getBanHistory';
|
|
||||||
export * from './getWarnHistory';
|
|
||||||
export * from './getWarnList';
|
|
||||||
export * from './grantReplayAccess';
|
|
||||||
export * from './updateAdminNotes';
|
|
||||||
export * from './viewLogHistory';
|
|
||||||
export * from './warnUser';
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
vi.mock('../../WebClient');
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import {
|
|
||||||
Command_BanFromServer_ext,
|
|
||||||
Command_ForceActivateUser_ext,
|
|
||||||
Command_GetAdminNotes_ext,
|
|
||||||
Command_GetBanHistory_ext,
|
|
||||||
Command_GetWarnHistory_ext,
|
|
||||||
Command_GetWarnList_ext,
|
|
||||||
Command_GrantReplayAccess_ext,
|
|
||||||
Command_UpdateAdminNotes_ext,
|
|
||||||
Command_ViewLogHistory_ext,
|
|
||||||
Command_ViewLogHistorySchema,
|
|
||||||
Command_WarnUser_ext,
|
|
||||||
Response_BanHistory_ext,
|
|
||||||
Response_GetAdminNotes_ext,
|
|
||||||
Response_ViewLogHistory_ext,
|
|
||||||
Response_WarnHistory_ext,
|
|
||||||
Response_WarnList_ext,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
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';
|
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Mock } from 'vitest';
|
|
||||||
|
|
||||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand as Mock,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('banFromServer', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_BanFromServer', () => {
|
|
||||||
banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1);
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_BanFromServer_ext,
|
|
||||||
expect.objectContaining({ minutes: 30, userName: 'alice' }),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.banFromServer', () => {
|
|
||||||
banFromServer(30, 'alice');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.moderator.banFromServer).toHaveBeenCalledWith('alice');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('forceActivateUser', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_ForceActivateUser', () => {
|
|
||||||
forceActivateUser('alice', 'mod1');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.forceActivateUser', () => {
|
|
||||||
forceActivateUser('alice', 'mod1');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.moderator.forceActivateUser).toHaveBeenCalledWith('alice', 'mod1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAdminNotes', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_GetAdminNotes', () => {
|
|
||||||
getAdminNotes('alice');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_GetAdminNotes_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.objectContaining({ responseExt: Response_GetAdminNotes_ext })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.getAdminNotes with notes', () => {
|
|
||||||
getAdminNotes('alice');
|
|
||||||
const resp = { notes: 'some notes' };
|
|
||||||
invokeOnSuccess(resp, { responseCode: 0 });
|
|
||||||
expect(WebClient.instance.response.moderator.getAdminNotes).toHaveBeenCalledWith('alice', 'some notes');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getBanHistory', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_GetBanHistory', () => {
|
|
||||||
getBanHistory('alice');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_GetBanHistory_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.objectContaining({ responseExt: Response_BanHistory_ext })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.banHistory with banList', () => {
|
|
||||||
getBanHistory('alice');
|
|
||||||
const resp = { banList: [{ id: 1 }] };
|
|
||||||
invokeOnSuccess(resp, { responseCode: 0 });
|
|
||||||
expect(WebClient.instance.response.moderator.banHistory).toHaveBeenCalledWith('alice', [{ id: 1 }]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getWarnHistory', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_GetWarnHistory', () => {
|
|
||||||
getWarnHistory('alice');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_GetWarnHistory_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.objectContaining({ responseExt: Response_WarnHistory_ext })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.warnHistory with warnList', () => {
|
|
||||||
getWarnHistory('alice');
|
|
||||||
const resp = { warnList: [{ id: 2 }] };
|
|
||||||
invokeOnSuccess(resp, { responseCode: 0 });
|
|
||||||
expect(WebClient.instance.response.moderator.warnHistory).toHaveBeenCalledWith('alice', [{ id: 2 }]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getWarnList', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_GetWarnList', () => {
|
|
||||||
getWarnList('mod1', 'alice', 'US');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_GetWarnList_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.objectContaining({ responseExt: Response_WarnList_ext })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.warnListOptions with the response', () => {
|
|
||||||
getWarnList('mod1', 'alice', 'US');
|
|
||||||
const resp = { warning: ['w1', 'w2'], userName: 'alice', userClientid: 'US' };
|
|
||||||
invokeOnSuccess(resp, { responseCode: 0 });
|
|
||||||
expect(WebClient.instance.response.moderator.warnListOptions).toHaveBeenCalledWith([resp]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('grantReplayAccess', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_GrantReplayAccess', () => {
|
|
||||||
grantReplayAccess(10, 'mod1');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.grantReplayAccess', () => {
|
|
||||||
grantReplayAccess(10, 'mod1');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.moderator.grantReplayAccess).toHaveBeenCalledWith(10, 'mod1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateAdminNotes', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => {
|
|
||||||
updateAdminNotes('alice', 'new notes');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.updateAdminNotes', () => {
|
|
||||||
updateAdminNotes('alice', 'new notes');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.moderator.updateAdminNotes).toHaveBeenCalledWith('alice', 'new notes');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('viewLogHistory', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_ViewLogHistory', () => {
|
|
||||||
const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 });
|
|
||||||
viewLogHistory(filters);
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_ViewLogHistory_ext,
|
|
||||||
expect.any(Object),
|
|
||||||
expect.objectContaining({ responseExt: Response_ViewLogHistory_ext })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.viewLogs with logMessage', () => {
|
|
||||||
const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 });
|
|
||||||
viewLogHistory(filters);
|
|
||||||
const resp = { logMessage: ['log1'] };
|
|
||||||
invokeOnSuccess(resp, { responseCode: 0 });
|
|
||||||
expect(WebClient.instance.response.moderator.viewLogs).toHaveBeenCalledWith(['log1']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('warnUser', () => {
|
|
||||||
|
|
||||||
it('calls sendModeratorCommand with Command_WarnUser', () => {
|
|
||||||
warnUser('alice', 'bad behavior', 'cid');
|
|
||||||
expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith(
|
|
||||||
Command_WarnUser_ext, expect.any(Object), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.moderator.warnUser', () => {
|
|
||||||
warnUser('alice', 'bad behavior', 'cid');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.moderator.warnUser).toHaveBeenCalledWith('alice');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function updateAdminNotes(userName: string, notes: string): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(
|
|
||||||
Command_UpdateAdminNotes_ext,
|
|
||||||
create(Command_UpdateAdminNotesSchema, { userName, notes }),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.moderator.updateAdminNotes(userName, notes);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema, Response_ViewLogHistory_ext } from '@app/generated';
|
|
||||||
import type { ViewLogHistoryParams } from '@app/generated';
|
|
||||||
|
|
||||||
export function viewLogHistory(filters: ViewLogHistoryParams): void {
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_ViewLogHistory_ext, create(Command_ViewLogHistorySchema, filters), {
|
|
||||||
responseExt: Response_ViewLogHistory_ext,
|
|
||||||
onSuccess: (response) => {
|
|
||||||
WebClient.instance.response.moderator.viewLogs(response.logMessage);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_WarnUser_ext, Command_WarnUserSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void {
|
|
||||||
const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages });
|
|
||||||
WebClient.instance.protobuf.sendModeratorCommand(Command_WarnUser_ext, cmd, {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.moderator.warnUser(userName);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_CreateGame_ext, Command_CreateGameSchema } from '@app/generated';
|
|
||||||
import type { CreateGameParams } from '@app/generated';
|
|
||||||
|
|
||||||
export function createGame(roomId: number, gameConfig: CreateGameParams): void {
|
|
||||||
WebClient.instance.protobuf.sendRoomCommand(roomId, Command_CreateGame_ext, create(Command_CreateGameSchema, gameConfig), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.room.gameCreated(roomId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './createGame';
|
|
||||||
export * from './joinGame';
|
|
||||||
export * from './leaveRoom';
|
|
||||||
export * from './roomSay';
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_JoinGame_ext, Command_JoinGameSchema, Response_ResponseCode } from '@app/generated';
|
|
||||||
import type { JoinGameParams } from '@app/generated';
|
|
||||||
|
|
||||||
// Desktop message strings from cockatrice/src/interface/widgets/server/game_selector.cpp:234-260
|
|
||||||
// (GameSelector::checkResponse). Codes not listed here (e.g. RespContextError) intentionally
|
|
||||||
// fall through to the desktop's `default:;` — silent to the user.
|
|
||||||
const ERROR_MESSAGES: Record<number, string> = {
|
|
||||||
[Response_ResponseCode.RespNotInRoom]: 'Please join the appropriate room first.',
|
|
||||||
[Response_ResponseCode.RespNameNotFound]: 'The game does not exist any more.',
|
|
||||||
[Response_ResponseCode.RespGameFull]: 'The game is already full.',
|
|
||||||
[Response_ResponseCode.RespWrongPassword]: 'Wrong password.',
|
|
||||||
[Response_ResponseCode.RespSpectatorsNotAllowed]: 'Spectators are not allowed in this game.',
|
|
||||||
[Response_ResponseCode.RespOnlyBuddies]: 'This game is only open to its creator\'s buddies.',
|
|
||||||
[Response_ResponseCode.RespUserLevelTooLow]: 'This game is only open to registered users.',
|
|
||||||
[Response_ResponseCode.RespInIgnoreList]: 'You are being ignored by the creator of this game.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function joinGame(roomId: number, joinGameParams: JoinGameParams): void {
|
|
||||||
const response = WebClient.instance.response.room;
|
|
||||||
response.setJoinGamePending(true);
|
|
||||||
|
|
||||||
const onResponseCode: { [code: number]: () => void } = {
|
|
||||||
// Match desktop default:; — acknowledge silently, no user dialog.
|
|
||||||
[Response_ResponseCode.RespContextError]: () => response.setJoinGamePending(false),
|
|
||||||
};
|
|
||||||
for (const codeStr of Object.keys(ERROR_MESSAGES)) {
|
|
||||||
const code = Number(codeStr);
|
|
||||||
onResponseCode[code] = () => response.setJoinGameError(code, ERROR_MESSAGES[code]);
|
|
||||||
}
|
|
||||||
|
|
||||||
WebClient.instance.protobuf.sendRoomCommand(
|
|
||||||
roomId,
|
|
||||||
Command_JoinGame_ext,
|
|
||||||
create(Command_JoinGameSchema, joinGameParams),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
response.setJoinGamePending(false);
|
|
||||||
response.joinedGame(roomId, joinGameParams.gameId);
|
|
||||||
},
|
|
||||||
onResponseCode,
|
|
||||||
onError: () => response.setJoinGamePending(false),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function leaveRoom(roomId: number): void {
|
|
||||||
WebClient.instance.protobuf.sendRoomCommand(roomId, Command_LeaveRoom_ext, create(Command_LeaveRoomSchema), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.room.leaveRoom(roomId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
vi.mock('../../WebClient');
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import {
|
|
||||||
Command_CreateGame_ext,
|
|
||||||
Command_CreateGameSchema,
|
|
||||||
Command_JoinGame_ext,
|
|
||||||
Command_JoinGameSchema,
|
|
||||||
Command_LeaveRoom_ext,
|
|
||||||
Command_RoomSay_ext,
|
|
||||||
Response_ResponseCode,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
import { createGame } from './createGame';
|
|
||||||
import { joinGame } from './joinGame';
|
|
||||||
import { leaveRoom } from './leaveRoom';
|
|
||||||
import { roomSay } from './roomSay';
|
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { Mock } from 'vitest';
|
|
||||||
|
|
||||||
const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
|
|
||||||
WebClient.instance.protobuf.sendRoomCommand as Mock,
|
|
||||||
// sendRoomCommand(roomId, ext, value, options) — options at index 3
|
|
||||||
3
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('createGame', () => {
|
|
||||||
|
|
||||||
it('calls sendRoomCommand with Command_CreateGame', () => {
|
|
||||||
createGame(5, create(Command_CreateGameSchema, { maxPlayers: 4 }));
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith(
|
|
||||||
5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.room.gameCreated with roomId', () => {
|
|
||||||
createGame(5, create(Command_CreateGameSchema, {}));
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.room.gameCreated).toHaveBeenCalledWith(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('joinGame', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
(WebClient.instance.response.room.joinedGame as Mock).mockClear();
|
|
||||||
(WebClient.instance.response.room.setJoinGamePending as Mock).mockClear();
|
|
||||||
(WebClient.instance.response.room.setJoinGameError as Mock).mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls sendRoomCommand with Command_JoinGame', () => {
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42, password: '' }));
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith(
|
|
||||||
7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches setJoinGamePending(true) before sending', () => {
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42 }));
|
|
||||||
expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess clears pending and calls response.room.joinedGame with roomId and gameId', () => {
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42 }));
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false);
|
|
||||||
expect(WebClient.instance.response.room.joinedGame).toHaveBeenCalledWith(7, 42);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Desktop GameSelector::checkResponse — matching message strings from
|
|
||||||
// cockatrice/src/interface/widgets/server/game_selector.cpp:234-260.
|
|
||||||
const errorCases: Array<[number, string]> = [
|
|
||||||
[Response_ResponseCode.RespNotInRoom, 'Please join the appropriate room first.'],
|
|
||||||
[Response_ResponseCode.RespNameNotFound, 'The game does not exist any more.'],
|
|
||||||
[Response_ResponseCode.RespGameFull, 'The game is already full.'],
|
|
||||||
[Response_ResponseCode.RespWrongPassword, 'Wrong password.'],
|
|
||||||
[Response_ResponseCode.RespSpectatorsNotAllowed, 'Spectators are not allowed in this game.'],
|
|
||||||
[Response_ResponseCode.RespOnlyBuddies, 'This game is only open to its creator\'s buddies.'],
|
|
||||||
[Response_ResponseCode.RespUserLevelTooLow, 'This game is only open to registered users.'],
|
|
||||||
[Response_ResponseCode.RespInIgnoreList, 'You are being ignored by the creator of this game.'],
|
|
||||||
];
|
|
||||||
|
|
||||||
it.each(errorCases)('code %i dispatches setJoinGameError with desktop-matching message', (code, message) => {
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42 }));
|
|
||||||
invokeResponseCode(code);
|
|
||||||
expect(WebClient.instance.response.room.setJoinGameError).toHaveBeenCalledWith(code, message);
|
|
||||||
expect(WebClient.instance.response.room.joinedGame).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('code 11 (RespContextError) is silent — clears pending, no setJoinGameError, no console.error', () => {
|
|
||||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42 }));
|
|
||||||
invokeResponseCode(Response_ResponseCode.RespContextError);
|
|
||||||
expect(WebClient.instance.response.room.setJoinGameError).not.toHaveBeenCalled();
|
|
||||||
expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false);
|
|
||||||
expect(consoleError).not.toHaveBeenCalled();
|
|
||||||
consoleError.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unknown response code goes to onError — clears pending, no setJoinGameError', () => {
|
|
||||||
joinGame(7, create(Command_JoinGameSchema, { gameId: 42 }));
|
|
||||||
invokeOnError(99);
|
|
||||||
expect(WebClient.instance.response.room.setJoinGameError).not.toHaveBeenCalled();
|
|
||||||
expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('leaveRoom', () => {
|
|
||||||
|
|
||||||
it('calls sendRoomCommand with Command_LeaveRoom', () => {
|
|
||||||
leaveRoom(3);
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith(
|
|
||||||
3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSuccess calls response.room.leaveRoom with roomId', () => {
|
|
||||||
leaveRoom(3);
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(WebClient.instance.response.room.leaveRoom).toHaveBeenCalledWith(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('roomSay', () => {
|
|
||||||
|
|
||||||
it('calls sendRoomCommand with trimmed message', () => {
|
|
||||||
roomSay(2, ' hello ');
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith(
|
|
||||||
2,
|
|
||||||
Command_RoomSay_ext,
|
|
||||||
expect.objectContaining({ message: 'hello' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call sendRoomCommand when message is blank', () => {
|
|
||||||
roomSay(2, ' ');
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call sendRoomCommand when message is empty string', () => {
|
|
||||||
roomSay(2, '');
|
|
||||||
expect(WebClient.instance.protobuf.sendRoomCommand).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import { Command_RoomSay_ext, Command_RoomSaySchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function roomSay(roomId: number, message: string): void {
|
|
||||||
const trimmed = message.trim();
|
|
||||||
|
|
||||||
if (!trimmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WebClient.instance.protobuf.sendRoomCommand(roomId, Command_RoomSay_ext, create(Command_RoomSaySchema, { message: trimmed }));
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_AccountEdit_ext, Command_AccountEditSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void {
|
|
||||||
const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country });
|
|
||||||
WebClient.instance.protobuf.sendSessionCommand(Command_AccountEdit_ext, cmd, {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.session.accountEditChanged(realName, email, country);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_AccountImage_ext, Command_AccountImageSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function accountImage(image: Uint8Array): void {
|
|
||||||
WebClient.instance.protobuf.sendSessionCommand(Command_AccountImage_ext, create(Command_AccountImageSchema, { image }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.session.accountImageChanged(image);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void {
|
|
||||||
const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword });
|
|
||||||
WebClient.instance.protobuf.sendSessionCommand(Command_AccountPassword_ext, cmd, {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.session.accountPasswordChange();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import {
|
|
||||||
Command_Activate_ext,
|
|
||||||
Command_ActivateSchema,
|
|
||||||
Response_ResponseCode,
|
|
||||||
type ActivateParams,
|
|
||||||
} from '@app/generated';
|
|
||||||
|
|
||||||
import { StatusEnum } from '../../types/StatusEnum';
|
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import type { ConnectTarget } from '../../types/WebClientConfig';
|
|
||||||
import { disconnect, login, updateStatus } from './';
|
|
||||||
|
|
||||||
export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void {
|
|
||||||
const { userName, token } = options;
|
|
||||||
|
|
||||||
WebClient.instance.protobuf.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, {
|
|
||||||
...CLIENT_CONFIG,
|
|
||||||
userName,
|
|
||||||
token,
|
|
||||||
}), {
|
|
||||||
onResponseCode: {
|
|
||||||
[Response_ResponseCode.RespActivationAccepted]: () => {
|
|
||||||
WebClient.instance.response.session.accountActivationSuccess();
|
|
||||||
login({
|
|
||||||
host: options.host,
|
|
||||||
port: options.port,
|
|
||||||
userName: options.userName,
|
|
||||||
}, password, passwordSalt);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed');
|
|
||||||
disconnect();
|
|
||||||
WebClient.instance.response.session.accountActivationFailed();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { create } from '@bufbuild/protobuf';
|
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
|
|
||||||
import { Command_AddToList_ext, Command_AddToListSchema } from '@app/generated';
|
|
||||||
|
|
||||||
export function addToBuddyList(userName: string): void {
|
|
||||||
addToList('buddy', userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addToIgnoreList(userName: string): void {
|
|
||||||
addToList('ignore', userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addToList(list: string, userName: string): void {
|
|
||||||
WebClient.instance.protobuf.sendSessionCommand(Command_AddToList_ext, create(Command_AddToListSchema, { list, userName }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
WebClient.instance.response.session.addToList(list, userName);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { WebClient } from '../../WebClient';
|
|
||||||
import type { ConnectTarget } from '../../types/WebClientConfig';
|
|
||||||
|
|
||||||
export function connect(target: ConnectTarget): void {
|
|
||||||
WebClient.instance.connect(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testConnect(target: ConnectTarget): void {
|
|
||||||
WebClient.instance.testConnect(target);
|
|
||||||
}
|
|
||||||
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