deadhead-space-freight-tycoon/apps/web/test/fleet-sleep.test.tsx

176 lines
6.4 KiB
TypeScript

import type { Pilot, PlayerState, ResolutionSummary } from "@deadhead/schema";
import { 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 { sleepLabel } from "../src/components/FleetSleep";
import { independentStatus } from "../src/components/IndependentPilots";
import { pilotExtras } from "./fixtures";
import { fakeApi, renderApp, samplePlayerState, shipExtras, comeBack } from "./render";
const NOW = Date.parse("2100-01-03T12:00:00Z");
const SLEPT = Date.parse("2100-01-02T09:30:00Z");
const rosa: Pilot = {
...samplePlayerState().pilots[0]!,
id: "p1",
name: "Rosa",
isAvatar: false,
assignedShipId: "s1",
locationId: "earth",
...pilotExtras,
mode: "independent",
};
const summary: ResolutionSummary = {
dispatchId: "d1",
shipId: "s1",
pilotId: "p1",
originId: "earth",
destinationId: "moon",
tutorial: false,
independent: true,
deadhead: false,
fuelCost: 100,
topUp: null,
goodId: "consumer-goods",
massTonnes: 6,
shipper: "Pacific Orbital Logistics",
credits: 600,
loanRepayment: 0,
pilotPay: 200,
pilotXp: 10,
discoveredLocationId: null,
scriptedId: null,
event: null,
restEndsAt: null,
resolvedAt: SLEPT - 60_000,
};
/** An owner with Rosa flying Independent; asleep since SLEPT, or awake. */
function owner(asleep: boolean, over: Partial<PlayerState> = {}, afterHours = 24): PlayerState {
return samplePlayerState({
tutorial: null,
bays: [{ id: "b", locationId: "earth" }],
pilots: [samplePlayerState().pilots[0]!, rosa],
ships: [
{
id: "s1",
modelId: "courier",
name: "Wren",
status: "idle",
locationId: "earth",
dispatchId: null,
lastArrivalAt: 0,
...shipExtras("courier"),
pilotId: "p1",
},
],
dispatchOffice: { owned: true, spendingFloor: 500 },
fleetSleep: { afterHours, sleepsAt: asleep ? SLEPT : NOW + 3_600_000, asleep },
...over,
});
}
/** A fake API whose state wakes up on "Back to work". */
function sleepyApi(over: Partial<PlayerState> = {}) {
let state = owner(true, over);
const api = fakeApi({
getPlayerState: () => state,
backToWork: () => {
state = owner(false, over);
return state;
},
acknowledgeSummaries: () => ({ acknowledged: 0 }),
});
return api;
}
describe("fleet sleep on screen (Step 24.5, decisions 0.5.137, 0.5.138)", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
vi.setSystemTime(NOW);
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
});
afterEach(() => vi.useRealTimers());
it("shows the Bridge status while asleep, and Back to work wakes the fleet", async () => {
const api = sleepyApi();
renderApp(api, "/");
const panel = await screen.findByLabelText("Fleet asleep");
expect(panel).toHaveTextContent("Fleet asleep — waiting for your orders");
await userEvent.click(within(panel).getByRole("button", { name: "Back to work" }));
expect(api.call).toHaveBeenCalledWith("backToWork", {});
await waitFor(() => expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument());
});
it("shows nothing on the Bridge while awake", async () => {
renderApp(fakeApi({ getPlayerState: () => owner(false) }), "/");
expect(await screen.findByRole("heading", { name: "Bridge" })).toBeInTheDocument();
expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
});
it("shows nothing on the Bridge when no pilot is Independent: nobody is waiting", async () => {
const manual = owner(true, {
pilots: [samplePlayerState().pilots[0]!, { ...rosa, mode: "manual" }],
});
renderApp(fakeApi({ getPlayerState: () => manual }), "/");
expect(await screen.findByRole("heading", { name: "Bridge" })).toBeInTheDocument();
expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
});
it("puts the sleep banner on the while-you-were-away screen, until Back to work", async () => {
comeBack();
const api = sleepyApi({ summaries: [summary] });
renderApp(api, "/");
const sheet = await screen.findByRole("dialog", { name: /While you were away/ });
const banner = within(sheet).getByLabelText("Fleet asleep");
expect(banner).toHaveTextContent(
/^Your fleet went quiet on .+, .+ after 24 h without orders\./,
);
await userEvent.click(within(banner).getByRole("button", { name: "Back to work" }));
await waitFor(() =>
expect(within(sheet).queryByLabelText("Fleet asleep")).not.toBeInTheDocument(),
);
});
it("leaves the banner off the away screen when the fleet didn't go quiet", async () => {
comeBack();
renderApp(fakeApi({ getPlayerState: () => owner(false, { summaries: [summary] }) }), "/");
const sheet = await screen.findByRole("dialog", { name: /While you were away/ });
expect(within(sheet).queryByLabelText("Fleet asleep")).not.toBeInTheDocument();
});
it("says Waiting for orders for a sleeping Independent pilot", () => {
const asleep = owner(true);
expect(independentStatus(asleep, rosa)).toBe("Waiting for orders");
expect(independentStatus(owner(false), rosa)).toBe("Waiting for new contracts at Earth");
});
it("saves the threshold in Settings → Company and shows the new one", async () => {
let state = owner(false);
const api = fakeApi({
getPlayerState: () => state,
setSleepThreshold: ({ hours }) => {
state = owner(false, {}, hours);
return state;
},
});
renderApp(api, "/settings");
const group = await screen.findByRole("group", { name: "Fleet goes quiet after" });
expect(
within(group)
.getAllByRole("radio")
.map((r) => r.parentElement!.textContent),
).toEqual(["12 h", "24 h", "48 h", "3 days"]);
expect(within(group).getByRole("radio", { name: "24 h" })).toBeChecked();
await userEvent.click(within(group).getByRole("radio", { name: "3 days" }));
expect(api.call).toHaveBeenCalledWith("setSleepThreshold", { hours: 72 });
await waitFor(() => expect(within(group).getByRole("radio", { name: "3 days" })).toBeChecked());
});
it("words the thresholds in hours, then days", () => {
expect([12, 24, 48, 72].map(sleepLabel)).toEqual(["12 h", "24 h", "48 h", "3 days"]);
});
});