Gold is the primary in both themes, Bricolage Grotesque carries the wordmark, titles and the score, and Movie A / Movie B wear the blue and magenta of three-strip Technicolor on their posters instead of colouring whole rows. The loop closes on screen - connectors light, a gold line draws back to Movie A, the row glows, then the result arrives - in place of confetti, on every play mode, scaled to the chain's length. Every page shares one header and hairline lists; copy is sentence case with one verb per flow, and the Home hero shows a real finished loop. Also clamps link counts at zero: an opponent who had not moved yet showed "-1 links".
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
import { renderHook } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import { useGameStore } from '@/stores/game-store';
|
|
import { useGameScreen } from './use-game-screen';
|
|
import type { ActorChainLink, MovieChainLink } from '@/types';
|
|
|
|
const movieLink = (id: number, title = `Movie ${id}`): MovieChainLink => ({
|
|
type: 'movie',
|
|
id,
|
|
title,
|
|
posterPath: null,
|
|
releaseDate: '2000-01-01',
|
|
popularity: 5,
|
|
});
|
|
const actorLink = (id: number, name = `Actor ${id}`): ActorChainLink => ({
|
|
type: 'actor',
|
|
id,
|
|
name,
|
|
profilePath: null,
|
|
popularity: 5,
|
|
});
|
|
|
|
const MOVIE_A = movieLink(100, 'Movie A');
|
|
const MOVIE_B = movieLink(200, 'Movie B');
|
|
|
|
beforeEach(() => {
|
|
useGameStore.getState().resetGame();
|
|
});
|
|
|
|
describe('useGameScreen', () => {
|
|
it('is not ready while the game is idle', () => {
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.ready).toBe(false);
|
|
expect(result.current.status).toBe('idle');
|
|
});
|
|
|
|
it('is not ready when either target movie is missing, even mid-game', () => {
|
|
useGameStore.setState({ status: 'playing', movieA: MOVIE_A, movieB: null });
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.ready).toBe(false);
|
|
});
|
|
|
|
it('is ready with both movies once the game is playing', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A],
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.ready).toBe(true);
|
|
if (result.current.ready) {
|
|
expect(result.current.movieA).toBe(MOVIE_A);
|
|
expect(result.current.movieB).toBe(MOVIE_B);
|
|
}
|
|
});
|
|
|
|
it('stays ready once the game is completed', () => {
|
|
useGameStore.setState({
|
|
status: 'completed',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A, actorLink(1), MOVIE_B, actorLink(2), MOVIE_A],
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.ready).toBe(true);
|
|
});
|
|
|
|
it('prompts for an actor after the last link is a movie', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A],
|
|
currentSearchMode: 'actor',
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.searchLabel).toBe('Who was in "Movie A"?');
|
|
expect(result.current.searchPlaceholder).toBe('Search for an actor…');
|
|
});
|
|
|
|
it('prompts for a movie after the last link is an actor', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A, actorLink(1, 'Keanu')],
|
|
currentSearchMode: 'movie',
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.searchLabel).toBe('What movie was Keanu also in?');
|
|
expect(result.current.searchPlaceholder).toBe('Search for a movie…');
|
|
});
|
|
|
|
it('has no label when the chain is empty', () => {
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.searchLabel).toBe('');
|
|
expect(result.current.searchPlaceholder).toBe('');
|
|
});
|
|
|
|
it('has no label when the search mode and last link type are mismatched mid-transition', () => {
|
|
useGameStore.setState({
|
|
chain: [MOVIE_A, actorLink(1)],
|
|
currentSearchMode: 'actor',
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.searchLabel).toBe('');
|
|
});
|
|
|
|
it('flags the loop hint once movie B has been passed and a movie is next', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A, actorLink(1), MOVIE_B, actorLink(2)],
|
|
currentSearchMode: 'movie',
|
|
passedMovieB: true,
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.showLoopHint).toBe(true);
|
|
});
|
|
|
|
it('does not flag the loop hint before movie B has been passed', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A],
|
|
currentSearchMode: 'movie',
|
|
passedMovieB: false,
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.showLoopHint).toBe(false);
|
|
});
|
|
|
|
it('does not flag the loop hint while searching for an actor', () => {
|
|
useGameStore.setState({
|
|
status: 'playing',
|
|
movieA: MOVIE_A,
|
|
movieB: MOVIE_B,
|
|
chain: [MOVIE_A, actorLink(1), MOVIE_B],
|
|
currentSearchMode: 'actor',
|
|
passedMovieB: true,
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.showLoopHint).toBe(false);
|
|
});
|
|
|
|
it('does not flag the loop hint without a movie A', () => {
|
|
useGameStore.setState({
|
|
status: 'idle',
|
|
movieA: null,
|
|
passedMovieB: true,
|
|
currentSearchMode: 'movie',
|
|
});
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.showLoopHint).toBe(false);
|
|
});
|
|
|
|
it('exposes the store actions and validating flag directly', () => {
|
|
useGameStore.setState({ isValidating: true });
|
|
const { result } = renderHook(() => useGameScreen());
|
|
expect(result.current.isValidating).toBe(true);
|
|
expect(result.current.undoLastLink).toBe(useGameStore.getState().undoLastLink);
|
|
expect(typeof result.current.validateAndAddActor).toBe('function');
|
|
expect(typeof result.current.validateAndAddMovie).toBe('function');
|
|
});
|
|
});
|