188 lines
6.0 KiB
TypeScript
188 lines
6.0 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { Dispatch, PlayerState, Ship } from "@deadhead/schema";
|
|
import { createRepositionRecord, dispatchProgress } from "@deadhead/sim";
|
|
import { act, screen, waitFor, 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 { formatCredits, formatDuration } from "../src/format";
|
|
import { fakeApi, renderApp, samplePlayerState } from "./render";
|
|
|
|
const NOW = Date.parse("2100-01-01T00:00:00Z");
|
|
|
|
const flight: Dispatch = {
|
|
id: "d1",
|
|
shipId: "s-fly",
|
|
pilotId: "p2",
|
|
contract: null,
|
|
originId: "earth",
|
|
destinationId: "moon",
|
|
tutorial: false,
|
|
departureAt: NOW - 60_000,
|
|
arrivalAt: NOW + 180_000,
|
|
distanceMillionKm: 0.39,
|
|
fuelCost: 50,
|
|
expectedPayout: 0,
|
|
seed: 1,
|
|
status: "in-flight",
|
|
resolvedAt: null,
|
|
};
|
|
|
|
const ship = (overrides: Partial<Ship>): Ship => ({
|
|
id: "s",
|
|
modelId: "courier",
|
|
name: "Ship",
|
|
status: "idle",
|
|
locationId: "earth",
|
|
dispatchId: null,
|
|
lastArrivalAt: 0,
|
|
pilotId: null,
|
|
...overrides,
|
|
});
|
|
|
|
function fleetState(overrides: Partial<PlayerState> = {}): PlayerState {
|
|
const base = samplePlayerState({ tutorial: null });
|
|
return {
|
|
...base,
|
|
bays: [
|
|
{ id: "b1", locationId: "earth" },
|
|
{ id: "b2", locationId: "moon" },
|
|
{ id: "b3", locationId: "mars" },
|
|
],
|
|
ships: [
|
|
ship({ id: "s-idle", name: "Dusty Mule", pilotId: "avatar" }),
|
|
ship({
|
|
id: "s-fly",
|
|
name: "Slow Ox",
|
|
status: "in-flight",
|
|
locationId: null,
|
|
dispatchId: "d1",
|
|
pilotId: "p2",
|
|
}),
|
|
],
|
|
pilots: [
|
|
{ ...base.pilots[0]!, assignedShipId: "s-idle" },
|
|
{
|
|
...base.pilots[0]!,
|
|
id: "p2",
|
|
name: "Rae Okafor",
|
|
isAvatar: false,
|
|
assignedShipId: "s-fly",
|
|
status: "in-flight",
|
|
locationId: null,
|
|
},
|
|
],
|
|
activeDispatches: [flight],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("fleet screen", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
|
vi.setSystemTime(NOW);
|
|
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
|
|
});
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("shows an in-flight ship's progress from the sim, and it advances", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => fleetState() }), "/fleet");
|
|
const bar = await screen.findByRole("progressbar", { name: "Slow Ox progress" });
|
|
expect(bar).toHaveAttribute(
|
|
"aria-valuenow",
|
|
String(Math.round(dispatchProgress(flight, NOW) * 100)),
|
|
);
|
|
expect(bar).toHaveAttribute("aria-valuenow", "25");
|
|
|
|
act(() => vi.advanceTimersByTime(60_000));
|
|
expect(bar).toHaveAttribute("aria-valuenow", "50");
|
|
expect(screen.getByText("Earth → Moon", { exact: false })).toBeInTheDocument();
|
|
expect(screen.getByText("2m 00s")).toBeInTheDocument();
|
|
});
|
|
|
|
it("lists each ship with model, location and pilot", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => fleetState() }), "/fleet");
|
|
const idle = (await screen.findByText("Dusty Mule")).closest("li")!;
|
|
expect(idle).toHaveTextContent("Courier");
|
|
expect(idle).toHaveTextContent("At Earth");
|
|
expect(idle).toHaveTextContent("Captain");
|
|
});
|
|
|
|
/** A crewed Courier idle at the Moon with no Moon bay; Mars discovered but out of range. */
|
|
const atMoon = () =>
|
|
fleetState({
|
|
bays: [{ id: "b1", locationId: "earth" }],
|
|
locations: [
|
|
{ id: "earth", discovered: true },
|
|
{ id: "moon", discovered: true },
|
|
{ id: "mars", discovered: true },
|
|
{ id: "ceres-station", discovered: false },
|
|
{ id: "jupiter-cloud-platform", discovered: false },
|
|
],
|
|
company: { id: "c", name: "D", credits: 10_000, loanBalance: 0 },
|
|
ships: [
|
|
ship({ id: "s-idle", name: "Dusty Mule", locationId: "moon", pilotId: "avatar" }),
|
|
fleetState().ships[1]!,
|
|
],
|
|
});
|
|
|
|
it("lets an idle ship with no bay where it is deadhead to a discovered location", async () => {
|
|
const api = fakeApi({ getPlayerState: atMoon });
|
|
renderApp(api, "/fleet");
|
|
const idle = (await screen.findByText("Dusty Mule")).closest("li")!;
|
|
await userEvent.click(within(idle).getByRole("button", { name: "Deadhead (fly empty)" }));
|
|
|
|
const picker = screen.getByRole("dialog", { name: "Deadhead Dusty Mule" });
|
|
const options = within(picker)
|
|
.getAllByRole("button")
|
|
.map((b) => b.getAttribute("aria-label"))
|
|
.filter((label) => label !== null);
|
|
expect(options).toEqual(["Earth", "Mars"]);
|
|
|
|
const mars = within(picker).getByRole("button", { name: "Mars" });
|
|
expect(mars).toBeDisabled();
|
|
expect(mars).toHaveTextContent("Out of range");
|
|
|
|
const earth = within(picker).getByRole("button", { name: "Earth" });
|
|
const record = createRepositionRecord(
|
|
{
|
|
id: "r",
|
|
ship: { id: "s-idle", modelId: "courier", locationId: "moon", inFlight: false },
|
|
pilot: {
|
|
id: "avatar",
|
|
isAvatar: true,
|
|
assignedShipId: "s-idle",
|
|
inFlight: false,
|
|
skills: {},
|
|
restEndsAt: null,
|
|
},
|
|
destinationId: "earth",
|
|
now: NOW,
|
|
worldSeed: 1,
|
|
},
|
|
content,
|
|
);
|
|
expect(earth).toBeEnabled();
|
|
expect(earth).toHaveTextContent(`${formatCredits(record.fuelCost)} fuel`);
|
|
expect(earth).toHaveTextContent(formatDuration(record.arrivalAt - record.departureAt));
|
|
|
|
await userEvent.click(earth);
|
|
await waitFor(() =>
|
|
expect(api.call).toHaveBeenCalledWith("createDispatch", {
|
|
kind: "reposition",
|
|
shipId: "s-idle",
|
|
pilotId: "avatar",
|
|
destinationId: "earth",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not offer a deadhead to in-flight ships", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => fleetState() }), "/fleet");
|
|
const flying = (await screen.findByText("Slow Ox")).closest("li")!;
|
|
expect(
|
|
within(flying).queryByRole("button", { name: "Deadhead (fly empty)" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
});
|