deadhead-space-freight-tycoon/apps/web/test/render.tsx

120 lines
3.7 KiB
TypeScript

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<N>) => RouteResponse<N> | Promise<RouteResponse<N>>;
};
/**
* 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> = {}): 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",
},
],
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(
<QueryClientProvider client={queryClient}>
<ApiProvider api={api}>
<MemoryRouter initialEntries={[path]}>
<App />
</MemoryRouter>
</ApiProvider>
</QueryClientProvider>,
);
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",
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,
};
}