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
22 changes: 18 additions & 4 deletions app/games/[id]/load-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,29 @@ export type LoadGameResult =
* A game IGDB returns with `hidden: true` (DLC, a bundle, an edition, the
* Erotic theme) is reported as not found: the catalog filter exists to keep
* those out of browsing, the way an adult movie is (spec 0025, rationale).
*/
export async function loadGame(id: number): Promise<LoadGameResult> {
const result = await readGameIncludingHidden(id);
return result.kind === "found" && result.game.hidden
? { kind: "not_found" }
: result;
}

/**
* `loadGame` without the hidden rule: a hidden game comes back as found,
* with `hidden: true`. The game status actions need the difference, because
* Want to play accepts a hidden game, so a listed game stays movable, while
* Playing and Finished refuse it (spec 0026, AC-9).
*
* Only an `IgdbError` becomes `failed`. Anything else, `IgdbValidationError`
* included, is a bug rather than an IGDB outcome, so it is rethrown instead
* of being dressed up as "Couldn't reach IGDB" (AC-8).
* of being dressed up as "Couldn't reach IGDB" (spec 0025, AC-8).
*/
export async function loadGame(id: number): Promise<LoadGameResult> {
export async function readGameIncludingHidden(
id: number,
): Promise<LoadGameResult> {
try {
const game = await getGame(id);
return game.hidden ? { kind: "not_found" } : { kind: "found", game };
return { kind: "found", game: await getGame(id) };
} catch (error) {
if (isIgdbNotFound(error)) return { kind: "not_found" };
if (error instanceof IgdbError) return { kind: "failed" };
Expand Down
22 changes: 21 additions & 1 deletion app/games/[id]/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";

import { GameDetailSkeleton } from "@/components/game/game-detail-skeleton";
import { GameHero } from "@/components/game/game-hero";
import { GameTrackingSlot } from "@/components/tracking/game-tracking-slot";

/**
* covers: spec 0025, AC-1, AC-6, AC-7, AC-8, AC-9, AC-10
Expand Down Expand Up @@ -148,7 +149,8 @@ describe("the streamed body", () => {

expect(loadGame).toHaveBeenCalledWith(1942);
expect(hero.type).toBe(GameHero);
expect(hero.props).toEqual({
const { tracking, ...facts } = hero.props as { tracking: Element };
expect(facts).toEqual({
name: FOUND.name,
coverUrl: FOUND.coverUrl,
releaseDate: FOUND.releaseDate,
Expand All @@ -160,6 +162,24 @@ describe("the streamed body", () => {
});
});

it("puts the status control in the hero's tracking slot, in its own null fallback boundary (spec 0026, AC-1)", async () => {
loadGame.mockResolvedValue({ kind: "found", game: FOUND });

const article = await bodyFor("1942");
const hero = article.props.children as Element;
const tracking = hero.props.tracking as Element;
const slot = tracking.props.children as Element;

expect(tracking.type).toBe(Suspense);
expect(tracking.props.fallback).toBeNull();
expect(slot.type).toBe(GameTrackingSlot);
expect(slot.props).toEqual({
gameId: 1942,
name: FOUND.name,
releaseDate: FOUND.releaseDate,
});
});

it("calls notFound for a game IGDB does not know or hides (AC-7)", async () => {
loadGame.mockResolvedValue({ kind: "not_found" });

Expand Down
15 changes: 13 additions & 2 deletions app/games/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GameDetailSkeleton } from "@/components/game/game-detail-skeleton";
import { GameHero } from "@/components/game/game-hero";
import { RetryLink } from "@/components/retry-link";
import { StatePanel } from "@/components/state-panel";
import { GameTrackingSlot } from "@/components/tracking/game-tracking-slot";
import { parseGameId } from "@/lib/catalog/ids";
import { truncateAtWord } from "@/lib/format";

Expand Down Expand Up @@ -60,8 +61,9 @@ export async function generateMetadata({
* the boundary.
*
* Nothing here reads a cookie, a header, the session or the visitor's region
* (AC-9). G4 adds the status control in its own boundary through the hero's
* `tracking` slot, and G9 adds the sections under the hero.
* (AC-9). The status control reads the session in its own Suspense boundary
* through the hero's `tracking` slot (spec 0026, AC-1), and G9 adds the
* sections under the hero.
*/
export default function GamePage({ params }: PageProps<"/games/[id]">) {
return (
Expand Down Expand Up @@ -110,6 +112,15 @@ async function GameDetail({
platforms={game.platforms}
communityRating={game.communityRating}
communityRatingCount={game.communityRatingCount}
tracking={
<Suspense fallback={null}>
<GameTrackingSlot
gameId={id}
name={game.name}
releaseDate={game.releaseDate}
/>
</Suspense>
}
/>
</article>
);
Expand Down
Loading
Loading