176 lines
6.4 KiB
TypeScript
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"]);
|
|
});
|
|
});
|