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 (locked)" })); 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"); }); });