mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-09 15:54:47 -07:00
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
import { act, fireEvent, screen } from '@testing-library/react';
|
|
import { create } from '@bufbuild/protobuf';
|
|
import {
|
|
renderWithProviders,
|
|
makeStoreState,
|
|
makeUser,
|
|
connectedWithRoomsState,
|
|
} from '../../../__test-utils__';
|
|
import { App, Data } from '@app/types';
|
|
import { GameTypes } from '@app/store';
|
|
import GameSelector from './GameSelector';
|
|
|
|
const { mockUseWebClient, mockNavigate } = vi.hoisted(() => ({
|
|
mockUseWebClient: vi.fn(),
|
|
mockNavigate: vi.fn(),
|
|
}));
|
|
vi.mock('@app/hooks', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@app/hooks')>();
|
|
return { ...actual, useWebClient: mockUseWebClient };
|
|
});
|
|
vi.mock('react-router-dom', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('react-router-dom')>();
|
|
return { ...actual, useNavigate: () => mockNavigate };
|
|
});
|
|
|
|
function makeRoomEntry(games: Data.ServerInfo_Game[] = [], gametypeMap: Record<number, string> = {}) {
|
|
return {
|
|
info: create(Data.ServerInfo_RoomSchema, { roomId: 1, name: 'Main' }),
|
|
gametypeMap,
|
|
order: 0,
|
|
games: Object.fromEntries(games.map((info) => [info.gameId, { info, gameType: '' }])),
|
|
users: {},
|
|
};
|
|
}
|
|
|
|
function makeGame(overrides: any = {}): Data.ServerInfo_Game {
|
|
return create(Data.ServerInfo_GameSchema, {
|
|
gameId: 1,
|
|
roomId: 1,
|
|
description: 'Test',
|
|
maxPlayers: 4,
|
|
playerCount: 1,
|
|
spectatorsAllowed: true,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function makeWebClient() {
|
|
return {
|
|
request: {
|
|
rooms: {
|
|
joinRoom: vi.fn(),
|
|
leaveRoom: vi.fn(),
|
|
roomSay: vi.fn(),
|
|
createGame: vi.fn(),
|
|
joinGame: vi.fn(),
|
|
},
|
|
},
|
|
} as any;
|
|
}
|
|
|
|
function buildState(
|
|
room: ReturnType<typeof makeRoomEntry>,
|
|
user = makeUser(),
|
|
selectedGameId?: number,
|
|
roomsOverrides: Partial<{ joinGamePending: boolean; joinGameError: { code: number; message: string } | null }> = {},
|
|
) {
|
|
return makeStoreState({
|
|
...connectedWithRoomsState,
|
|
rooms: {
|
|
rooms: { 1: room },
|
|
joinedRoomIds: { 1: true },
|
|
joinedGameIds: {},
|
|
messages: { 1: [] },
|
|
sortGamesBy: { field: App.GameSortField.START_TIME, order: App.SortDirection.DESC },
|
|
sortUsersBy: { field: App.UserSortField.NAME, order: App.SortDirection.ASC },
|
|
selectedGameIds: selectedGameId != null ? { 1: selectedGameId } : {},
|
|
gameFilters: {},
|
|
joinGamePending: false,
|
|
joinGameError: null,
|
|
...roomsOverrides,
|
|
} as any,
|
|
server: {
|
|
...(connectedWithRoomsState.server as any),
|
|
user,
|
|
} as any,
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockUseWebClient.mockReset();
|
|
mockNavigate.mockReset();
|
|
});
|
|
|
|
describe('GameSelector', () => {
|
|
it('renders the count header from getRoomGameCounts', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([makeGame({ gameId: 1 }), makeGame({ gameId: 2 })]);
|
|
renderWithProviders(<GameSelector room={room as any} />, { preloadedState: buildState(room) });
|
|
expect(screen.getByText('Games shown: 2 / 2')).toBeInTheDocument();
|
|
});
|
|
|
|
it('Join button is disabled until a game is selected', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([makeGame({ gameId: 1 })]);
|
|
renderWithProviders(<GameSelector room={room as any} />, { preloadedState: buildState(room) });
|
|
expect(screen.getByRole('button', { name: /^Join$/ })).toBeDisabled();
|
|
});
|
|
|
|
it('Join button enabled and dispatches joinGame when a game is selected', () => {
|
|
const client = makeWebClient();
|
|
mockUseWebClient.mockReturnValue(client);
|
|
const game = makeGame({ gameId: 7, withPassword: false });
|
|
const room = makeRoomEntry([game]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), 7),
|
|
});
|
|
|
|
const joinBtn = screen.getByRole('button', { name: /^Join$/ });
|
|
expect(joinBtn).not.toBeDisabled();
|
|
fireEvent.click(joinBtn);
|
|
|
|
expect(client.request.rooms.joinGame).toHaveBeenCalledTimes(1);
|
|
expect(client.request.rooms.joinGame).toHaveBeenCalledWith(1, expect.objectContaining({
|
|
gameId: 7,
|
|
spectator: false,
|
|
joinAsJudge: false,
|
|
password: '',
|
|
}));
|
|
});
|
|
|
|
it('clicking Join on a password-protected game opens the password prompt before sending', () => {
|
|
const client = makeWebClient();
|
|
mockUseWebClient.mockReturnValue(client);
|
|
const game = makeGame({ gameId: 8, withPassword: true });
|
|
const room = makeRoomEntry([game]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), 8),
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /^Join$/ }));
|
|
|
|
expect(client.request.rooms.joinGame).not.toHaveBeenCalled();
|
|
expect(screen.getByText('Password required')).toBeInTheDocument();
|
|
|
|
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'hunter2' } });
|
|
// The dialog has its own "Join" button — find both and click the last (in dialog).
|
|
const joinButtons = screen.getAllByRole('button', { name: /^Join$/ });
|
|
fireEvent.click(joinButtons[joinButtons.length - 1]);
|
|
|
|
expect(client.request.rooms.joinGame).toHaveBeenCalledTimes(1);
|
|
expect(client.request.rooms.joinGame).toHaveBeenCalledWith(1, expect.objectContaining({
|
|
gameId: 8,
|
|
password: 'hunter2',
|
|
}));
|
|
});
|
|
|
|
it('Spectate button is disabled when spectators are not allowed', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const game = makeGame({ gameId: 1, spectatorsAllowed: false });
|
|
const room = makeRoomEntry([game]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), 1),
|
|
});
|
|
expect(screen.getByRole('button', { name: /Join as Spectator/i })).toBeDisabled();
|
|
});
|
|
|
|
it('Join is disabled when the selected game is full', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const game = makeGame({ gameId: 1, playerCount: 4, maxPlayers: 4 });
|
|
const room = makeRoomEntry([game]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), 1),
|
|
});
|
|
expect(screen.getByRole('button', { name: /^Join$/ })).toBeDisabled();
|
|
});
|
|
|
|
it('judge buttons are hidden when the user is not a judge', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser({ userLevel: 0 })),
|
|
});
|
|
expect(screen.queryByRole('button', { name: /Join as Judge$/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('judge buttons are visible when the user has the IsJudge flag', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser({ userLevel: Data.ServerInfo_User_UserLevelFlag.IsJudge })),
|
|
});
|
|
expect(screen.getByRole('button', { name: /Join as Judge$/i })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /Join as Judge Spectator/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders AlertDialog with the join error message from state', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), undefined, {
|
|
joinGameError: { code: 10, message: 'The game is already full.' },
|
|
}),
|
|
});
|
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
expect(screen.getByText('The game is already full.')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /^ok$/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render AlertDialog when joinGameError is null (covers silent RespContextError)', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), undefined, { joinGameError: null }),
|
|
});
|
|
// Only the CreateGame / FilterGames / PromptDialog / AlertDialog dialogs might exist; none
|
|
// should be open, so no role="dialog" in the DOM.
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('clicking Join on a game already present in games.games navigates to /game without sending a command', () => {
|
|
const client = makeWebClient();
|
|
mockUseWebClient.mockReturnValue(client);
|
|
const game = makeGame({ gameId: 7, withPassword: false });
|
|
const room = makeRoomEntry([game]);
|
|
const state = buildState(room, makeUser(), 7);
|
|
(state as any).games = { games: { 7: { info: { gameId: 7 } } } };
|
|
renderWithProviders(<GameSelector room={room as any} />, { preloadedState: state });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /^Join$/ }));
|
|
|
|
expect(client.request.rooms.joinGame).not.toHaveBeenCalled();
|
|
expect(mockNavigate).toHaveBeenCalledWith(App.RouteEnum.GAME);
|
|
});
|
|
|
|
it('dispatching GAME_JOINED navigates to /game (mirrors JOIN_ROOM → /room)', async () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const room = makeRoomEntry([]);
|
|
const { store } = renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room),
|
|
});
|
|
|
|
mockNavigate.mockClear();
|
|
await act(async () => {
|
|
store.dispatch({
|
|
type: GameTypes.GAME_JOINED,
|
|
payload: { data: { gameInfo: { gameId: 42 }, hostId: 0, playerId: 0, spectator: false } },
|
|
});
|
|
});
|
|
expect(mockNavigate).toHaveBeenCalledWith(App.RouteEnum.GAME);
|
|
});
|
|
|
|
it('Join button is disabled while joinGamePending is true even when a game is selected', () => {
|
|
mockUseWebClient.mockReturnValue(makeWebClient());
|
|
const game = makeGame({ gameId: 7 });
|
|
const room = makeRoomEntry([game]);
|
|
renderWithProviders(<GameSelector room={room as any} />, {
|
|
preloadedState: buildState(room, makeUser(), 7, { joinGamePending: true }),
|
|
});
|
|
expect(screen.getByRole('button', { name: /^Join$/ })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: /Join as Spectator/i })).toBeDisabled();
|
|
});
|
|
|
|
it('clicking Create then submitting forwards createGame', () => {
|
|
const client = makeWebClient();
|
|
mockUseWebClient.mockReturnValue(client);
|
|
const room = makeRoomEntry([]);
|
|
renderWithProviders(<GameSelector room={room as any} />, { preloadedState: buildState(room) });
|
|
fireEvent.click(screen.getByRole('button', { name: /^Create$/ }));
|
|
// The dialog Submit button is the second matching "Create" button
|
|
const createButtons = screen.getAllByRole('button', { name: /^Create$/ });
|
|
fireEvent.click(createButtons[createButtons.length - 1]);
|
|
expect(client.request.rooms.createGame).toHaveBeenCalledTimes(1);
|
|
expect(client.request.rooms.createGame.mock.calls[0][0]).toBe(1);
|
|
});
|
|
});
|