Files
movieloop-frontend/src/hooks/use-game-screen.test.ts
T
TehRiehlDeal 8d0b7f39ee feat(ui): give the app one visual identity as Movie Loop
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".
2026-09-12 22:42:55 -07:00

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');
});
});