import type { RouteName, RouteRequest, RouteResponse } from "@deadhead/schema"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render } from "@testing-library/react"; import { MemoryRouter } from "react-router"; import { vi } from "vitest"; import { App } from "../src/App"; import { ApiError, type ApiClient } from "../src/api/client"; import { ApiProvider } from "../src/api/context"; type Handlers = { [N in RouteName]?: (input: RouteRequest) => RouteResponse | Promise>; }; /** * A fake API: each route answers from its handler. Unhandled routes fail with * `not-found`; `getServerTime` defaults to the device clock. */ export function fakeApi(handlers: Handlers) { const call = vi.fn(async (name: RouteName, input: unknown) => { if (name === "getServerTime" && !handlers.getServerTime) { return { realTime: Date.now(), gameTime: 0 }; } const handler = handlers[name] as ((input: unknown) => unknown) | undefined; if (!handler) throw new ApiError(404, "not-found", `No fake for ${name}`); return handler(input); }); return { call } as unknown as ApiClient & { call: typeof call }; } export const unauthorized = () => { throw new ApiError(401, "unauthorized", "Sign in to continue."); }; export { samplePlayerState, shipExtras } from "./fixtures"; /** Renders the whole app at `path` with a fresh query cache and the given API. */ export function renderApp(api: ApiClient, path = "/") { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); const result = render( , ); return { ...result, queryClient }; } /** A tutorial board like the server's: light, medium and full Earth→Moon runs. */ export function sampleTutorialBoard( now = Date.parse("2100-01-01T00:00:00Z"), options: { scriptedMedium?: boolean } = {}, ) { const contract = (slot: number, massTonnes: number, payout: number) => ({ id: `tutorial:earth:1:0:${slot}`, originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes, payout, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: now, expiresAt: now + 3_600_000, firstContact: false, }); const medium = contract(1, 12, options.scriptedMedium ? 461 : 230); return { contracts: [ contract(0, 5, 96), options.scriptedMedium ? { ...medium, scriptedId: "unmarked-crates" } : medium, contract(2, 20, 384), ], periodEndsAt: now + 3_600_000, }; }