Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions frontend/src/game/GameConfigScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ 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()} />);
Expand Down Expand Up @@ -109,15 +115,30 @@ describe('GameConfigScreen landing', () => {
expect(screen.getByTestId('game-network-preview')).toBeInTheDocument();
});

it('hides settings by default and reveals timer / difficulty / studies on toggle', () => {
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.getByText(/Difficulty \(network\)/)).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} />);
Expand All @@ -140,6 +161,7 @@ describe('GameConfigScreen — France THT mode', () => {
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();
Expand All @@ -156,6 +178,7 @@ describe('GameConfigScreen — France THT mode', () => {
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'));
Expand All @@ -171,6 +194,7 @@ describe('GameConfigScreen — France THT mode', () => {

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' } });
Expand All @@ -181,6 +205,7 @@ describe('GameConfigScreen — France THT mode', () => {

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();
});
Expand All @@ -189,6 +214,7 @@ describe('GameConfigScreen — France THT mode', () => {
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();
Expand All @@ -208,6 +234,7 @@ describe('GameConfigScreen — France EHV (Matpower) mode', () => {
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'));
Expand All @@ -223,6 +250,7 @@ describe('GameConfigScreen — France EHV (Matpower) mode', () => {

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();
});
Expand Down
113 changes: 68 additions & 45 deletions frontend/src/game/GameConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,12 @@ export default function GameConfigScreen({ onStart }: GameConfigScreenProps) {
const [previewError, setPreviewError] = useState(false);

// Top-level mode: the European demo grid (curated reference studies) vs one
// of the difficulty-graded scenario databases (sampled by level).
// of the difficulty-graded scenario databases (sampled by level). The demo
// grid is the only mode a participant sees by default — France THT and
// France EHV (Matpower) stay hidden behind this toggle inside Configure
// settings so the landing page doesn't ask newcomers to choose a dataset
// family they don't yet have context for.
const [showModePicker, setShowModePicker] = useState(false);
const [mode, setMode] = useState<GameMode>('demo');
const [thtDifficulty, setThtDifficulty] = useState<Rte7000Difficulty>('easy');
const [numCases, setNumCases] = useState(5);
Expand Down Expand Up @@ -301,39 +306,6 @@ export default function GameConfigScreen({ onStart }: GameConfigScreenProps) {
under settings.
</p>

{/* Mode: European demo grid vs France THT graded scenarios. */}
<div style={card}>
<label style={labelStyle}>Mode</label>
<div style={{ display: 'flex', gap: space[2] }}>
<button data-testid="game-mode-demo"
style={{
...btn(mode === 'demo' ? colors.brand : colors.surfaceMuted,
mode === 'demo' ? colors.textOnBrand : colors.textSecondary),
flex: 1, padding: `${space[2]} ${space[3]}`, textAlign: 'left',
}}
onClick={() => setMode('demo')}>
🌍 European grid — demo
<div style={{ fontSize: text.xs, fontWeight: 400, marginTop: space.half, opacity: 0.85 }}>
The pan-European reference studies (and the French worst-case set).
</div>
</button>
{Object.values(GRADED).map((fam) => (
<button key={fam.key} data-testid={`game-mode-${fam.key}`}
style={{
...btn(mode === fam.key ? colors.brand : colors.surfaceMuted,
mode === fam.key ? colors.textOnBrand : colors.textSecondary),
flex: 1, padding: `${space[2]} ${space[3]}`, textAlign: 'left',
}}
onClick={() => setMode(fam.key)}>
{fam.button}
<div style={{ fontSize: text.xs, fontWeight: 400, marginTop: space.half, opacity: 0.85 }}>
{fam.buttonBlurb}
</div>
</button>
))}
</div>
</div>

{/* Landing — the only things a participant needs to start. */}
<div style={card}>
<div>
Expand Down Expand Up @@ -526,18 +498,69 @@ export default function GameConfigScreen({ onStart }: GameConfigScreenProps) {
<input type="number" min={1} max={3} value={maxActions} style={{ ...inputStyle, width: 80 }}
onChange={(e) => setMaxActions(Math.min(3, Math.max(1, Number(e.target.value))))} />
</div>
{mode === 'demo' && (
<div style={{ flex: 1, minWidth: 220 }}>
<label style={labelStyle}>Difficulty (network)</label>
<select style={inputStyle} value={difficulty}
onChange={(e) => changeDifficulty(e.target.value as Difficulty)}>
{DIFFICULTY_TIERS.map((t) => (
<option key={t.id} value={t.id}>{t.label}</option>
))}
</select>
</div>
)}
<div>
<label style={labelStyle}>Mode</label>
<button data-testid="game-mode-picker-toggle"
style={{
...btn(colors.surfaceMuted, colors.textSecondary),
border: `1px solid ${colors.border}`,
}}
onClick={() => setShowModePicker((v) => !v)}>
{showModePicker ? 'Hide other modes ▲' : '🌐 Other modes / regions ▼'}
</button>
</div>
</div>

{/* Hidden by default — France THT and France EHV (Matpower)
are advanced/graded datasets a newcomer doesn't need to
see on first load. Revealed verbatim (same buttons +
blurbs) by the toggle above. */}
{showModePicker && (
<div style={{ marginTop: space[3] }}>
<label style={labelStyle}>Mode</label>
<div style={{ display: 'flex', gap: space[2] }}>
<button data-testid="game-mode-demo"
style={{
...btn(mode === 'demo' ? colors.brand : colors.surfaceMuted,
mode === 'demo' ? colors.textOnBrand : colors.textSecondary),
flex: 1, padding: `${space[2]} ${space[3]}`, textAlign: 'left',
}}
onClick={() => setMode('demo')}>
🌍 European grid — demo
<div style={{ fontSize: text.xs, fontWeight: 400, marginTop: space.half, opacity: 0.85 }}>
The pan-European reference studies (and the French worst-case set).
</div>
</button>
{Object.values(GRADED).map((fam) => (
<button key={fam.key} data-testid={`game-mode-${fam.key}`}
style={{
...btn(mode === fam.key ? colors.brand : colors.surfaceMuted,
mode === fam.key ? colors.textOnBrand : colors.textSecondary),
flex: 1, padding: `${space[2]} ${space[3]}`, textAlign: 'left',
}}
onClick={() => setMode(fam.key)}>
{fam.button}
<div style={{ fontSize: text.xs, fontWeight: 400, marginTop: space.half, opacity: 0.85 }}>
{fam.buttonBlurb}
</div>
</button>
))}
</div>
</div>
)}

{showModePicker && mode === 'demo' && (
<div style={{ marginTop: space[3], maxWidth: 320 }}>
<label style={labelStyle}>Difficulty (network)</label>
<select style={inputStyle} value={difficulty}
onChange={(e) => changeDifficulty(e.target.value as Difficulty)}>
{DIFFICULTY_TIERS.map((t) => (
<option key={t.id} value={t.id}>{t.label}</option>
))}
</select>
</div>
)}

<p style={{ color: colors.textTertiary, fontSize: text.xs, margin: `${space[2]} 0 0` }}>
{graded ? graded.configHint : tier.blurb}
</p>
Expand Down
Loading