deadhead-space-freight-tycoon/apps/web/test/map-interactions.test.tsx

156 lines
5.5 KiB
TypeScript

import type { Dispatch, PlayerState } from "@deadhead/schema";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useClock } from "../src/clock/clock";
import { DEFAULT_CAMERA, toScreen } from "../src/map/camera";
import { hitTest } from "../src/map/hitTest";
import { buildScene } from "../src/map/scene";
import { fakeApi, renderApp, samplePlayerState } from "./render";
const NOW = Date.parse("2100-01-01T00:00:00Z");
const flight: Dispatch = {
id: "d1",
shipId: "s1",
pilotId: "p-rae",
contract: null,
originId: "earth",
destinationId: "mars",
tutorial: false,
departureAt: NOW - 3_600_000,
arrivalAt: NOW + 5 * 60_000,
distanceMillionKm: 100,
fuelCost: 0,
expectedPayout: 0,
seed: 1,
status: "in-flight",
resolvedAt: null,
};
function mapState(): PlayerState {
const base = samplePlayerState({ tutorial: null });
return {
...base,
bays: [{ id: "b1", locationId: "moon" }],
ships: [
{
id: "s1",
modelId: "freighter",
name: "Slow Ox",
status: "in-flight",
locationId: null,
dispatchId: "d1",
pilotId: "p-rae",
},
],
pilots: [
...base.pilots,
{
...base.pilots[0]!,
id: "p-rae",
name: "Rae Okafor",
isAvatar: false,
assignedShipId: "s1",
status: "in-flight",
locationId: null,
},
],
activeDispatches: [flight],
};
}
describe("map hit-testing", () => {
const viewport = { width: 375, height: 690 };
it("selects the location under a tap, and nothing on empty space", () => {
const scene = buildScene(mapState(), NOW);
// Ceres: nothing else is near it (the test flight is almost at Mars).
const ceres = scene.locations.find((l) => l.id === "ceres-station")!;
const p = toScreen(ceres.point, viewport, DEFAULT_CAMERA);
expect(hitTest(scene, { x: p.x + 5, y: p.y - 5 }, viewport, DEFAULT_CAMERA)).toEqual({
kind: "location",
id: "ceres-station",
});
expect(hitTest(scene, { x: 2, y: 2 }, viewport, DEFAULT_CAMERA)).toBeNull();
});
it("selects a ship marker under a tap", () => {
const scene = buildScene(mapState(), NOW);
const p = toScreen(scene.flights[0]!.marker, viewport, DEFAULT_CAMERA);
expect(hitTest(scene, p, viewport, DEFAULT_CAMERA)).toEqual({
kind: "flight",
dispatchId: "d1",
});
});
});
describe("map panels", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
vi.setSystemTime(NOW);
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
});
afterEach(() => vi.useRealTimers());
const openMap = async () => {
renderApp(fakeApi({ getPlayerState: mapState }), "/map");
return screen.findByTestId("map-canvas");
};
it("tapping a location with a bay shows its details and a link to its contract board", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Moon" }));
const panel = screen.getByRole("dialog", { name: "Moon" });
expect(panel).toHaveTextContent("Airless body");
expect(panel).toHaveTextContent(/from Earth/);
expect(within(panel).getByRole("link", { name: "View contract board" })).toHaveAttribute(
"href",
"/contracts?bay=moon",
);
});
it("tapping a discovered location without a bay shows no board link", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Earth" }));
const panel = screen.getByRole("dialog", { name: "Earth" });
expect(panel).toHaveTextContent("No bay here yet.");
expect(within(panel).queryByRole("link")).not.toBeInTheDocument();
});
it("tapping an undiscovered location hides its details", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Mars" }));
expect(screen.getByRole("dialog", { name: "Mars" })).toHaveTextContent(
"Undiscovered — deliver here to unlock.",
);
});
it("tapping a ship shows its route, pilot and ETA", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap ship s1" }));
const panel = screen.getByRole("dialog", { name: "Slow Ox" });
expect(panel).toHaveTextContent("Earth → Mars");
expect(panel).toHaveTextContent("Pilot: Rae Okafor");
expect(panel).toHaveTextContent("Arrives in 5m 00s");
});
it("tapping empty space or Close dismisses the panel", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Moon" }));
await userEvent.click(within(canvas).getByRole("button", { name: "Tap empty space" }));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Earth" }));
await userEvent.click(screen.getByRole("button", { name: "Close" }));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("the board link opens Contracts with that bay selected", async () => {
const canvas = await openMap();
await userEvent.click(within(canvas).getByRole("button", { name: "Tap Moon" }));
await userEvent.click(screen.getByRole("link", { name: "View contract board" }));
expect(await screen.findByLabelText("Bay")).toHaveValue("moon");
});
});