import type { PlayerState, 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."); }; /** A brand-new tutorial player, as guest sign-up returns it. */ export function samplePlayerState(overrides: Partial = {}): PlayerState { return { company: { id: "co1", name: "Deadhead Freight", credits: 0, loanBalance: 0 }, ships: [], pilots: [ { id: "avatar", name: "Captain", isAvatar: true, xp: 0, level: 0, skills: {}, skillPointsAvailable: 0, assignedShipId: null, status: "idle", locationId: "earth", dutyHours: 0, dutyLimit: 12, resting: false, restEndsAt: null, }, ], bays: [], locations: [ { id: "earth", discovered: true }, { id: "moon", discovered: true }, { id: "mars", discovered: false }, { id: "ceres-station", discovered: false }, { id: "jupiter-cloud-platform", discovered: false }, ], tutorial: { loanerLocationId: "earth", runInFlight: false, runsCompleted: 0, dealAvailable: false, }, activeDispatches: [], summaries: [], goals: [], ...overrides, }; } /** 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, }; }