Cockatrice/webclient/integration/src/websocket/deck.spec.ts
2026-04-19 16:36:33 -05:00

186 lines
No EOL
6.1 KiB
TypeScript

// Deck and replay command round-trips — validates the session command pipeline
// for deck CRUD and replay operations end-to-end.
import { create } from '@bufbuild/protobuf';
import { describe, expect, it } from 'vitest';
import { Data } from '@app/types';
import { store } from '@app/store';
import { SessionCommands } from '@app/websocket';
import { connectAndLogin } from '../helpers/setup';
import {
buildResponse,
buildResponseMessage,
deliverMessage,
} from '../helpers/protobuf-builders';
import { findLastSessionCommand } from '../helpers/command-capture';
describe('deck operations', () => {
it('populates backendDecks from deckList response', () => {
connectAndLogin();
SessionCommands.deckList();
const { cmdId } = findLastSessionCommand(Data.Command_DeckList_ext);
const deckFile = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 1,
name: 'MyDeck.cod',
file: create(Data.ServerInfo_DeckStorage_FileSchema, { creationTime: 1000 }),
});
const root = create(Data.ServerInfo_DeckStorage_FolderSchema, {
items: [deckFile],
});
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
ext: Data.Response_DeckList_ext,
value: create(Data.Response_DeckListSchema, { root }),
})));
const backendDecks = store.getState().server.backendDecks;
expect(backendDecks).not.toBeNull();
expect(backendDecks?.root?.items).toHaveLength(1);
expect(backendDecks?.root?.items[0]?.name).toBe('MyDeck.cod');
});
it('populates downloadedDeck from deckDownload response', () => {
connectAndLogin();
SessionCommands.deckDownload(42);
const { cmdId } = findLastSessionCommand(Data.Command_DeckDownload_ext);
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
ext: Data.Response_DeckDownload_ext,
value: create(Data.Response_DeckDownloadSchema, { deck: '4 Lightning Bolt\n20 Mountain' }),
})));
const downloaded = store.getState().server.downloadedDeck;
expect(downloaded).not.toBeNull();
expect(downloaded?.deckId).toBe(42);
expect(downloaded?.deck).toContain('Lightning Bolt');
});
it('deckUpload sends payload and dispatches uploadServerDeck on success', () => {
connectAndLogin();
SessionCommands.deckUpload('/folder', 0, '4 Counterspell\n20 Island');
const { cmdId, value } = findLastSessionCommand(Data.Command_DeckUpload_ext);
expect(value.path).toBe('/folder');
expect(value.deckList).toContain('Counterspell');
const newFile = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 7,
name: 'CounterDeck.cod',
});
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
ext: Data.Response_DeckUpload_ext,
value: create(Data.Response_DeckUploadSchema, { newFile }),
})));
// No state assertion: backendDecks is keyed by full tree, not single
// upload — the integration verifies the dispatcher is reached, not the
// tree-merge logic which lives in the reducer.
});
it('deckDel sends deckId and resolves on RespOk', () => {
connectAndLogin();
SessionCommands.deckDel(13);
const { cmdId, value } = findLastSessionCommand(Data.Command_DeckDel_ext);
expect(value.deckId).toBe(13);
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
})));
});
it('deckNewDir sends path + dirName payload and resolves on RespOk', () => {
connectAndLogin();
SessionCommands.deckNewDir('/parent', 'NewFolder');
const { cmdId, value } = findLastSessionCommand(Data.Command_DeckNewDir_ext);
expect(value.path).toBe('/parent');
expect(value.dirName).toBe('NewFolder');
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
})));
});
it('deckDelDir sends path payload and resolves on RespOk', () => {
connectAndLogin();
SessionCommands.deckDelDir('/folder/to/remove');
const { cmdId, value } = findLastSessionCommand(Data.Command_DeckDelDir_ext);
expect(value.path).toBe('/folder/to/remove');
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
})));
});
});
describe('replay operations', () => {
it('populates replays from replayList response', () => {
connectAndLogin();
SessionCommands.replayList();
const { cmdId } = findLastSessionCommand(Data.Command_ReplayList_ext);
const match = create(Data.ServerInfo_ReplayMatchSchema, {
gameId: 99,
gameName: 'Casual Game',
});
deliverMessage(buildResponseMessage(buildResponse({
cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
ext: Data.Response_ReplayList_ext,
value: create(Data.Response_ReplayListSchema, { matchList: [match] }),
})));
const replays = store.getState().server.replays;
expect(replays[99]).toBeDefined();
expect(replays[99].gameName).toBe('Casual Game');
});
it('removes replay from state on replayDeleteMatch round-trip', () => {
connectAndLogin();
// First populate a replay
SessionCommands.replayList();
const list = findLastSessionCommand(Data.Command_ReplayList_ext);
deliverMessage(buildResponseMessage(buildResponse({
cmdId: list.cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
ext: Data.Response_ReplayList_ext,
value: create(Data.Response_ReplayListSchema, {
matchList: [create(Data.ServerInfo_ReplayMatchSchema, { gameId: 99, gameName: 'Old Game' })],
}),
})));
expect(store.getState().server.replays[99]).toBeDefined();
// Now delete it
SessionCommands.replayDeleteMatch(99);
const del = findLastSessionCommand(Data.Command_ReplayDeleteMatch_ext);
deliverMessage(buildResponseMessage(buildResponse({
cmdId: del.cmdId,
responseCode: Data.Response_ResponseCode.RespOk,
})));
expect(store.getState().server.replays[99]).toBeUndefined();
});
});