Co-Study4Grid / frontend /src /game /GameConfigScreen.test.tsx
github-actions[bot]
Deploy f7b8bf6
1c730d1
Raw
History Blame Contribute Delete
13.1 kB
// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import GameConfigScreen from './GameConfigScreen';
const mockApi = vi.hoisted(() => ({ getPlayerSessions: vi.fn() }));
vi.mock('../api', () => ({ api: mockApi }));
beforeEach(() => {
mockApi.getPlayerSessions.mockResolvedValue({
player: 'amarot', session_count: 2,
session_names: ['amarot — session 1', 'amarot — session 2'],
});
});
afterEach(() => { cleanup(); vi.clearAllMocks(); });
const sessionInput = () => screen.getByTestId('game-session-name') as HTMLInputElement;
/** France THT / Matpower live behind Configure settings' mode toggle. */
const openModePicker = () => {
fireEvent.click(screen.getByTestId('game-settings-toggle'));
fireEvent.click(screen.getByTestId('game-mode-picker-toggle'));
};
describe('GameConfigScreen landing', () => {
it('keeps Start disabled until a player name is entered', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
expect(screen.getByTestId('game-start')).toBeDisabled();
});
it('auto-fills the session name from the player + next session index', async () => {
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
expect(mockApi.getPlayerSessions).toHaveBeenCalledWith('amarot');
});
it('falls back to session 1 when the backend is unreachable', async () => {
mockApi.getPlayerSessions.mockRejectedValue(new Error('offline'));
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'zoe' } });
await waitFor(() => expect(sessionInput().value).toBe('zoe — session 1'));
});
it('does not overwrite a session name the user typed', async () => {
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(sessionInput(), { target: { value: 'My custom run' } });
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await new Promise((r) => setTimeout(r, 400));
expect(sessionInput().value).toBe('My custom run');
// The names are still fetched (they drive the duplicate block), but the
// typed name is preserved.
expect(mockApi.getPlayerSessions).toHaveBeenCalledWith('amarot');
});
it('auto-suggests the first FREE index, skipping gaps in the recorded names', async () => {
// Recorded {1, 3} → the count-plus-one heuristic would collide on 3; the
// names-based suggestion fills the gap and picks 2.
mockApi.getPlayerSessions.mockResolvedValue({
player: 'amarot', session_count: 2,
session_names: ['amarot — session 1', 'amarot — session 3'],
});
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 2'));
});
it('suggests the next index after finishing a session (no collision)', async () => {
// Sessions 1-3 already recorded → the next free index is 4, never a
// re-suggested 3 (the reported bug).
mockApi.getPlayerSessions.mockResolvedValue({
player: 'amarot', session_count: 3,
session_names: ['amarot — session 1', 'amarot — session 2', 'amarot — session 3'],
});
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 4'));
});
it('blocks Start and shows an error when the name collides with an existing session', async () => {
const onStart = vi.fn();
render(<GameConfigScreen onStart={onStart} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
// Type a name that already exists (case-insensitive).
fireEvent.change(sessionInput(), { target: { value: 'Amarot — Session 1' } });
await waitFor(() => expect(screen.getByTestId('game-session-name-error')).toBeInTheDocument());
expect(screen.getByTestId('game-start')).toBeDisabled();
fireEvent.click(screen.getByTestId('game-start'));
expect(onStart).not.toHaveBeenCalled();
});
it('re-enables Start once the colliding name is changed to a free one', async () => {
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
fireEvent.change(sessionInput(), { target: { value: 'amarot — session 2' } });
await waitFor(() => expect(screen.getByTestId('game-start')).toBeDisabled());
fireEvent.change(sessionInput(), { target: { value: 'amarot — session 9' } });
expect(screen.queryByTestId('game-session-name-error')).toBeNull();
expect(screen.getByTestId('game-start')).not.toBeDisabled();
});
it('lists the configured studies and shows the network preview', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
expect(screen.getByTestId('game-studies-summary').querySelectorAll('li').length)
.toBeGreaterThan(0);
expect(screen.getByTestId('game-network-preview')).toBeInTheDocument();
});
it('hides settings by default and reveals timer / mode toggle / studies on toggle', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
expect(screen.queryByText(/Time limit per study/)).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('game-settings-toggle'));
expect(screen.getByText(/Time limit per study/)).toBeInTheDocument();
expect(screen.getByTestId('game-mode-picker-toggle')).toBeInTheDocument();
expect(screen.getByText(/Studies \(/)).toBeInTheDocument();
});
it('keeps other modes and the network-difficulty picker hidden until the mode toggle is used', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
fireEvent.click(screen.getByTestId('game-settings-toggle'));
expect(screen.queryByTestId('game-mode-tht')).not.toBeInTheDocument();
expect(screen.queryByTestId('game-mode-matpower')).not.toBeInTheDocument();
expect(screen.queryByText(/Difficulty \(network\)/)).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('game-mode-picker-toggle'));
expect(screen.getByTestId('game-mode-demo')).toBeInTheDocument();
expect(screen.getByTestId('game-mode-tht')).toBeInTheDocument();
expect(screen.getByTestId('game-mode-matpower')).toBeInTheDocument();
// Demo is still the active mode, so its network-difficulty select shows too.
expect(screen.getByText(/Difficulty \(network\)/)).toBeInTheDocument();
});
it('starts a session with the entered config', async () => {
const onStart = vi.fn();
render(<GameConfigScreen onStart={onStart} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
fireEvent.click(screen.getByTestId('game-start'));
expect(onStart).toHaveBeenCalledTimes(1);
const cfg = onStart.mock.calls[0][0];
expect(cfg.player).toBe('amarot');
expect(cfg.sessionName).toBe('amarot — session 3');
expect(cfg.assistance).toBe(true);
expect(cfg.timerSeconds).toBe(300);
expect(cfg.studies.length).toBeGreaterThan(0);
});
});
describe('GameConfigScreen — France THT mode', () => {
it('reveals the difficulty + case-count pickers and hides the demo studies/preview', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
// Demo is the default: its studies summary + network preview are shown.
expect(screen.getByTestId('game-studies-summary')).toBeInTheDocument();
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-tht'));
expect(screen.getByTestId('game-tht-difficulty')).toBeInTheDocument();
expect(screen.getByTestId('game-tht-count')).toBeInTheDocument();
expect(screen.getByTestId('game-tht-summary')).toBeInTheDocument();
// France THT shows its own network map (the shared RTE7000 backbone).
expect(screen.getByTestId('game-tht-preview')).toBeInTheDocument();
// The demo studies list + per-network preview belong to demo mode only.
expect(screen.queryByTestId('game-studies-summary')).not.toBeInTheDocument();
expect(screen.queryByTestId('game-network-preview')).not.toBeInTheDocument();
});
it('starts a France THT session by sampling the chosen number of cases', async () => {
const onStart = vi.fn();
render(<GameConfigScreen onStart={onStart} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-tht'));
fireEvent.change(screen.getByTestId('game-tht-count'), { target: { value: '3' } });
fireEvent.click(screen.getByTestId('game-start'));
expect(onStart).toHaveBeenCalledTimes(1);
const cfg = onStart.mock.calls[0][0];
expect(cfg.studies).toHaveLength(3);
for (const s of cfg.studies) {
expect(s.networkPath).toContain('data/rte7000_tht/grids/');
expect(s.contingencyElementId).toBeTruthy();
}
});
it('caps the number of cases at the pool size', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-tht'));
const count = screen.getByTestId('game-tht-count') as HTMLInputElement;
fireEvent.change(count, { target: { value: '999999' } });
// Clamped to the difficulty's available cases (a positive, finite pool).
expect(Number(count.value)).toBeGreaterThan(0);
expect(Number(count.value)).toBeLessThan(999999);
});
it('still requires a player name to start in THT mode', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-tht'));
expect(screen.getByTestId('game-start')).toBeDisabled();
});
});
describe('GameConfigScreen — France EHV (Matpower) mode', () => {
it('is a third top-level mode, exclusive with demo and France THT', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-matpower'));
expect(screen.getByTestId('game-matpower-difficulty')).toBeInTheDocument();
expect(screen.getByTestId('game-matpower-count')).toBeInTheDocument();
expect(screen.getByTestId('game-matpower-summary')).toBeInTheDocument();
expect(screen.getByTestId('game-matpower-preview')).toBeInTheDocument();
// The other two modes' panels are gone.
expect(screen.queryByTestId('game-tht-summary')).not.toBeInTheDocument();
expect(screen.queryByTestId('game-studies-summary')).not.toBeInTheDocument();
// ...and switching back to France THT restores its own panel.
fireEvent.click(screen.getByTestId('game-mode-tht'));
expect(screen.getByTestId('game-tht-summary')).toBeInTheDocument();
expect(screen.queryByTestId('game-matpower-summary')).not.toBeInTheDocument();
});
it('samples its studies from the Matpower grids, not the THT ones', async () => {
const onStart = vi.fn();
render(<GameConfigScreen onStart={onStart} />);
fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-matpower'));
fireEvent.change(screen.getByTestId('game-matpower-count'), { target: { value: '1' } });
fireEvent.click(screen.getByTestId('game-start'));
expect(onStart).toHaveBeenCalledTimes(1);
const cfg = onStart.mock.calls[0][0];
expect(cfg.studies).toHaveLength(1);
for (const s of cfg.studies) {
expect(s.networkPath).toContain('data/rte_matpower/grids/');
expect(s.contingencyElementId).toBeTruthy();
}
});
it('still requires a player name to start', () => {
render(<GameConfigScreen onStart={vi.fn()} />);
openModePicker();
fireEvent.click(screen.getByTestId('game-mode-matpower'));
expect(screen.getByTestId('game-start')).toBeDisabled();
});
});