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, shipExtras } 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", lastArrivalAt: 0, ...shipExtras("freighter"), 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, its contracts link and the HQ", 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 contracts" })).toHaveAttribute( "href", "/contracts?location=moon", ); expect(panel).toHaveTextContent("Your HQ is here."); }); it("tapping a discovered location without a bay still links to its contracts", 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 HQ here."); expect(within(panel).getByRole("link", { name: "View contracts" })).toHaveAttribute( "href", "/contracts?location=earth", ); }); 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 location selected", async () => { const canvas = await openMap(); await userEvent.click(within(canvas).getByRole("button", { name: "Tap Moon" })); await userEvent.click(screen.getByRole("link", { name: "View contracts" })); expect(await screen.findByRole("button", { name: /Moon/, pressed: true })).toBeInTheDocument(); }); });