fix join game

This commit is contained in:
seavor 2026-04-20 00:54:03 -05:00
parent 2afa2922e9
commit 515dff6d7b
2 changed files with 70 additions and 5 deletions

View file

@ -7,13 +7,21 @@ import {
connectedWithRoomsState,
} from '../../../__test-utils__';
import { App, Data } from '@app/types';
import { GameTypes } from '@app/store';
import GameSelector from './GameSelector';
const { mockUseWebClient } = vi.hoisted(() => ({ mockUseWebClient: vi.fn() }));
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 {
@ -81,6 +89,7 @@ function buildState(
beforeEach(() => {
mockUseWebClient.mockReset();
mockNavigate.mockReset();
});
describe('GameSelector', () => {
@ -209,6 +218,38 @@ describe('GameSelector', () => {
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();
store.dispatch({
type: GameTypes.GAME_JOINED,
payload: { data: { gameInfo: { gameId: 42 }, hostId: 0, playerId: 0, spectator: false } },
});
await Promise.resolve();
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 });

View file

@ -1,10 +1,18 @@
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { RoomsDispatch, RoomsSelectors, ServerSelectors, useAppSelector } from '@app/store';
import { useWebClient } from '@app/hooks';
import type { App, Enriched } from '@app/types';
import {
GameSelectors,
GameTypes,
RoomsDispatch,
RoomsSelectors,
ServerSelectors,
useAppSelector,
} from '@app/store';
import { useReduxEffect, useWebClient } from '@app/hooks';
import { App, type Enriched } from '@app/types';
import { AlertDialog, CreateGameDialog, FilterGamesDialog, PromptDialog } from '@app/dialogs';
import OpenGames from '../OpenGames';
@ -25,6 +33,7 @@ interface PendingPasswordJoin {
const GameSelector = ({ room }: GameSelectorProps) => {
const roomId = room.info.roomId;
const webClient = useWebClient();
const navigate = useNavigate();
const selectedGameId = useAppSelector((state) => RoomsSelectors.getSelectedGameId(state, roomId));
const selectedGame = useAppSelector((state) =>
@ -36,6 +45,13 @@ const GameSelector = ({ room }: GameSelectorProps) => {
const isJudgeUser = useAppSelector(ServerSelectors.getIsUserJudge);
const joinPending = useAppSelector(RoomsSelectors.getJoinGamePending);
const joinError = useAppSelector(RoomsSelectors.getJoinGameError);
const activeGameIds = useAppSelector(GameSelectors.getActiveGameIds);
// Mirrors Server.tsx's JOIN_ROOM → navigate(ROOM) pattern: when Event_GameJoined
// lands, we're actually in the game — route to /game.
useReduxEffect(() => {
navigate(App.RouteEnum.GAME);
}, GameTypes.GAME_JOINED, [navigate]);
const [createOpen, setCreateOpen] = useState(false);
const [filterOpen, setFilterOpen] = useState(false);
@ -43,6 +59,14 @@ const GameSelector = ({ room }: GameSelectorProps) => {
const sendJoin = useCallback(
(gameId: number, asSpectator: boolean, asJudge: boolean, password: string) => {
// Mirrors Rooms.tsx short-circuit: if we already have a live game entry
// (Event_GameJoined has populated games.games[gameId]), skip the duplicate
// JoinGame — the server would reject it with RespContextError — and go
// straight to the game view.
if (activeGameIds.includes(gameId)) {
navigate(App.RouteEnum.GAME);
return;
}
const params: App.JoinGameParams = {
gameId,
password,
@ -52,7 +76,7 @@ const GameSelector = ({ room }: GameSelectorProps) => {
};
webClient.request.rooms.joinGame(roomId, params);
},
[roomId, webClient],
[activeGameIds, navigate, roomId, webClient],
);
const beginJoin = useCallback(