import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import ChessGame from "./ChessGame";
import { Tutor } from "./Tutor";
interface MockChessboardProps {
options: {
onPieceDrop?: (move: { sourceSquare: string; targetSquare: string | null }) => void;
};
}
interface MockStartOptions {
personality: { name: string };
color: 'white' | 'black' | 'random';
}
// Mock dependencies
jest.mock("react-chessboard", () => ({
Chessboard: ({ options }: MockChessboardProps) => (
{
// Simulate a move drop
if (options.onPieceDrop) {
options.onPieceDrop({ sourceSquare: "e2", targetSquare: "e4" });
}
}}>
Chessboard Mock
),
}));
jest.mock("../lib/stockfish", () => {
const evaluate = jest.fn().mockResolvedValue({
score: 0.5,
mate: null,
bestMove: "e7e5",
depth: 15
});
return {
__mock: { evaluate },
Stockfish: jest.fn().mockImplementation(() => ({
evaluate,
terminate: jest.fn(),
})),
};
});
const { __mock: stockfishMock } = jest.requireMock("../lib/stockfish") as { __mock: { evaluate: jest.Mock } };
jest.mock("./Tutor", () => ({
Tutor: jest.fn(({ currentFen, userMove, computerMove, evalP0, evalP2, openingData, language }) => (
Tutor Mock (Fen: {currentFen})
{userMove && User Move: {userMove.san}}
{computerMove && Computer Move: {computerMove.san}}
{evalP0 && Eval P0: {evalP0.score}}
{evalP2 && Eval P2: {evalP2.score}}
{openingData && Opening: {openingData.name}}
Language: {language}
)),
}));
jest.mock("./GameAnalysisModal", () => ({
GameAnalysisModal: () => Analysis Modal Mock
,
}));
jest.mock("./GameOverModal", () => ({
GameOverModal: ({ onAnalyze }: { onAnalyze: () => void }) => (
Game Over Modal Mock
),
}));
jest.mock("./StartScreen", () => ({
__esModule: true,
default: ({ onStartGame }: { onStartGame: (options: MockStartOptions) => void }) => (
),
}));
describe("ChessGame Component", () => {
const mockPersonality = {
id: "test",
name: "Test Personality",
systemPrompt: "You are a helpful assistant.",
image: "🤖",
description: "Test description",
};
beforeEach(() => {
localStorage.clear();
jest.clearAllMocks();
jest.useFakeTimers();
stockfishMock.evaluate.mockResolvedValue({
score: 0.5,
mate: null,
bestMove: "e7e5",
depth: 15
});
});
it("renders the game board and tutor", async () => {
await act(async () => {
render(
{}}
/>
);
});
expect(screen.getByTestId("chessboard")).toBeInTheDocument();
expect(screen.getByTestId("tutor")).toBeInTheDocument();
});
it("handles user move and triggers analysis", async () => {
render(
{}}
/>
);
const mockedTutor = jest.mocked(Tutor);
const initialCalls = mockedTutor.mock.calls.length;
// Make a move by clicking the mock chessboard
await act(async () => {
fireEvent.click(screen.getByTestId("chessboard"));
jest.runAllTimers();
});
// Wait for the component to update
await waitFor(() => {
expect(mockedTutor.mock.calls.length).toBeGreaterThan(initialCalls);
});
});
it("restores a PGN game and persists save data without apiKey", async () => {
await act(async () => {
render(
{}}
/>
);
});
await waitFor(() => {
const saved = JSON.parse(localStorage.getItem("chess_tutor_save") || "{}");
expect(saved.id).toBe("restore-game");
expect(saved.pgn).toContain("1. e4 e5");
expect(saved).not.toHaveProperty("apiKey");
});
});
it("undoes cleanly while analysis is in flight", async () => {
render(
{}}
/>
);
await act(async () => {
fireEvent.click(screen.getByTestId("chessboard"));
});
await act(async () => {
fireEvent.click(screen.getByText(/undo/i));
jest.runAllTimers();
});
await waitFor(() => {
const tutor = screen.getByTestId("tutor");
expect(tutor).not.toHaveTextContent("Computer Move:");
});
});
it("ignores rapid repeated drops once the turn has switched", async () => {
render(
{}}
/>
);
await waitFor(() => {
expect(stockfishMock.evaluate).toHaveBeenCalled();
});
await waitFor(() => {
expect(screen.getByTestId("tutor")).toHaveTextContent("Eval P0: 0.5");
});
stockfishMock.evaluate.mockClear();
await act(async () => {
fireEvent.click(screen.getByTestId("chessboard"));
fireEvent.click(screen.getByTestId("chessboard"));
jest.runAllTimers();
});
await waitFor(() => {
expect(stockfishMock.evaluate.mock.calls.length).toBeGreaterThanOrEqual(1);
});
const playerTriggeredEvaluations = stockfishMock.evaluate.mock.calls.filter(([fen]: [string]) => typeof fen === "string");
expect(playerTriggeredEvaluations.length).toBeLessThanOrEqual(2);
});
});