228 lines
7.3 KiB
TypeScript
228 lines
7.3 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { Contract, Pilot, PlayerState, ResolutionSummary } from "@deadhead/schema";
|
|
import { act, render, screen, within } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useClock } from "../src/clock/clock";
|
|
import { CatchingUp, pilotLogbook } from "../src/components/PilotLogbook";
|
|
import { logbookFrom, pilotExtras } from "./fixtures";
|
|
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
|
|
|
const PERIOD_START = Date.parse("2100-01-01T00:00:00Z");
|
|
const FIRST_LOOK = content.config.independentPilots.firstLookRealMinutes * 60_000;
|
|
|
|
const job = (over: Partial<Contract> = {}): Contract => ({
|
|
id: "earth:1:0",
|
|
originId: "earth",
|
|
destinationId: "moon",
|
|
cargoTypeId: "standard",
|
|
goodId: "consumer-goods",
|
|
shipper: "Pacific Orbital Logistics",
|
|
priority: false,
|
|
deliveryWindowGameHours: null,
|
|
tier: 0,
|
|
massTonnes: 6,
|
|
payout: 1_234,
|
|
requiredSkill: null,
|
|
requiredLongHaulLevel: 0,
|
|
offeredAt: PERIOD_START,
|
|
expiresAt: PERIOD_START + 600_000,
|
|
firstContact: false,
|
|
...over,
|
|
});
|
|
|
|
const pilot = (id: string, name: string, over: Partial<Pilot> = {}): Pilot => ({
|
|
id,
|
|
name,
|
|
isAvatar: false,
|
|
isVeteran: false,
|
|
xp: 0,
|
|
level: 0,
|
|
skills: {},
|
|
skillPointsAvailable: 0,
|
|
assignedShipId: `ship-${id}`,
|
|
status: "idle",
|
|
locationId: "earth",
|
|
dutyHours: 0,
|
|
dutyLimit: 12,
|
|
resting: false,
|
|
restEndsAt: null,
|
|
...pilotExtras,
|
|
mode: "independent",
|
|
...over,
|
|
});
|
|
|
|
function owner(pilots: Pilot[]): PlayerState {
|
|
const base = samplePlayerState({ tutorial: null });
|
|
return {
|
|
...base,
|
|
bays: [{ id: "b", locationId: "earth" }],
|
|
pilots: [base.pilots[0]!, ...pilots],
|
|
ships: pilots.map((p) => ({
|
|
id: `ship-${p.id}`,
|
|
modelId: "courier",
|
|
name: `Ship ${p.id}`,
|
|
status: "idle",
|
|
locationId: "earth",
|
|
dispatchId: null,
|
|
lastArrivalAt: 0,
|
|
...shipExtras("courier"),
|
|
pilotId: p.id,
|
|
})),
|
|
dispatchOffice: { owned: true, spendingFloor: 500 },
|
|
};
|
|
}
|
|
|
|
describe("first-look chip (Step 21.7, decision 0.5.126)", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
|
vi.setSystemTime(PERIOD_START + 29_000);
|
|
useClock.setState({ offsetMs: 0, synced: true, now: PERIOD_START + 29_000 });
|
|
});
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
const show = (state: PlayerState, contracts: Contract[]) =>
|
|
renderApp(
|
|
fakeApi({
|
|
getPlayerState: () => state,
|
|
getContractBoard: () => ({ contracts, periodEndsAt: PERIOD_START + 600_000 }),
|
|
}),
|
|
"/contracts?location=earth",
|
|
);
|
|
|
|
it("counts down the player's first look, then disappears", async () => {
|
|
show(owner([pilot("p1", "Ada")]), [job()]);
|
|
const card = (await screen.findByText("1,234 cr")).closest("li")!;
|
|
expect(within(card).getByLabelText("Yours for")).toHaveTextContent("Yours for 02:31");
|
|
|
|
act(() => {
|
|
vi.setSystemTime(PERIOD_START + FIRST_LOOK + 1_000);
|
|
useClock.setState({ now: PERIOD_START + FIRST_LOOK + 1_000 });
|
|
});
|
|
expect(within(card).queryByLabelText("Yours for")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("isn't shown without Independent pilots", async () => {
|
|
show(owner([pilot("p1", "Ada", { mode: "manual" })]), [job()]);
|
|
const card = (await screen.findByText("1,234 cr")).closest("li")!;
|
|
expect(within(card).queryByLabelText("Yours for")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("isn't shown on a first contact, which is always the player's", async () => {
|
|
show(owner([pilot("p1", "Ada")]), [job({ firstContact: true, destinationId: "mars" })]);
|
|
const card = (await screen.findByText("1,234 cr")).closest("li")!;
|
|
expect(within(card).queryByLabelText("Yours for")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("catching up indicator (Step 21.7)", () => {
|
|
beforeEach(() => vi.useFakeTimers());
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("appears only when a catch-up runs longer than half a second", () => {
|
|
const { rerender } = render(<CatchingUp busy />);
|
|
act(() => vi.advanceTimersByTime(400));
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
act(() => vi.advanceTimersByTime(200));
|
|
expect(screen.getByRole("status")).toHaveTextContent("Catching up on your fleet…");
|
|
rerender(<CatchingUp busy={false} />);
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
|
|
// A quick pass never shows it.
|
|
rerender(<CatchingUp busy />);
|
|
act(() => vi.advanceTimersByTime(300));
|
|
rerender(<CatchingUp busy={false} />);
|
|
act(() => vi.advanceTimersByTime(1_000));
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("pilot logbook (Step 21.7)", () => {
|
|
const summary = (over: Partial<ResolutionSummary>): ResolutionSummary => ({
|
|
dispatchId: "d",
|
|
shipId: "ship-p1",
|
|
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: 200,
|
|
pilotPay: 200,
|
|
pilotXp: 10,
|
|
discoveredLocationId: null,
|
|
scriptedId: null,
|
|
event: null,
|
|
restEndsAt: null,
|
|
resolvedAt: PERIOD_START,
|
|
...over,
|
|
});
|
|
const ada = pilot("p1", "Ada");
|
|
const bo = pilot("p2", "Bo", { locationId: "moon" });
|
|
const summaries = [
|
|
summary({ dispatchId: "a1", resolvedAt: 1 }),
|
|
summary({
|
|
dispatchId: "a2",
|
|
deadhead: true,
|
|
goodId: null,
|
|
massTonnes: null,
|
|
shipper: null,
|
|
credits: 0,
|
|
loanRepayment: 0,
|
|
pilotPay: 0,
|
|
pilotXp: 0,
|
|
fuelCost: 40,
|
|
resolvedAt: 2,
|
|
}),
|
|
summary({
|
|
dispatchId: "b1",
|
|
pilotId: "p2",
|
|
shipId: "ship-p2",
|
|
destinationId: "earth",
|
|
originId: "moon",
|
|
credits: 900,
|
|
pilotPay: 300,
|
|
loanRepayment: 0,
|
|
pilotXp: 12,
|
|
restEndsAt: PERIOD_START + 3_600_000,
|
|
resolvedAt: 3,
|
|
}),
|
|
// The player's own delivery isn't in the pilots' logbook.
|
|
summary({ dispatchId: "you", pilotId: "avatar", independent: false, resolvedAt: 4 }),
|
|
];
|
|
|
|
// The logbook is built from entries now (CR-025): these stand for the summaries above.
|
|
const entries = () => logbookFrom({ ...owner([ada, bo]), summaries }).entries;
|
|
|
|
it("lists each pilot's jobs, deadheads, rests and XP, newest first, and where they are now", () => {
|
|
const book = pilotLogbook(owner([ada, bo]), entries());
|
|
expect(book.pilots.map((p) => p.name)).toEqual(["Captain", "Ada", "Bo"]);
|
|
const [, a, b] = book.pilots;
|
|
expect(a!.entries.map((e) => e.kind)).toEqual(["flight", "flight"]);
|
|
expect(a!.entries[1]).toMatchObject({ goodId: "consumer-goods", payout: 1_000, pilotPay: 200 });
|
|
expect(a!.xp).toBe(10);
|
|
expect(a!.now).toBe("At Earth");
|
|
expect(b!.entries.map((e) => e.kind)).toEqual(["rest", "flight"]);
|
|
expect(b!.now).toBe("At Moon");
|
|
});
|
|
|
|
it("totals the entries for the company", () => {
|
|
const book = pilotLogbook(owner([ada, bo]), entries());
|
|
expect(book.totals).toEqual({
|
|
jobs: 3,
|
|
deadheads: 1,
|
|
payout: 1_000 + 1_200 + 1_000,
|
|
pilotShare: 200 + 300 + 200,
|
|
fuelCost: 100 + 40 + 100 + 100,
|
|
loanRepaid: 200 + 200,
|
|
net: 1_000 + 1_200 + 1_000 - 700 - 340 - 400,
|
|
xp: 32,
|
|
});
|
|
});
|
|
});
|