174 lines
6.3 KiB
TypeScript
174 lines
6.3 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { Dispatch, PlayerState, Ship } from "@deadhead/schema";
|
|
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 { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
|
|
|
const NOW = Date.parse("2100-01-01T00:00:00Z");
|
|
|
|
const ship = (id: string, status: Ship["status"]): Ship => ({
|
|
id,
|
|
modelId: "courier",
|
|
name: id,
|
|
status,
|
|
locationId: status === "idle" ? "earth" : null,
|
|
dispatchId: status === "idle" ? null : `d-${id}`,
|
|
lastArrivalAt: 0,
|
|
...shipExtras("courier"),
|
|
pilotId: null,
|
|
});
|
|
|
|
const dispatch = (shipId: string, arrivalAt: number): Dispatch => ({
|
|
id: `d-${shipId}`,
|
|
shipId,
|
|
pilotId: "avatar",
|
|
contract: null,
|
|
originId: "earth",
|
|
destinationId: "moon",
|
|
tutorial: false,
|
|
departureAt: NOW - 60_000,
|
|
arrivalAt,
|
|
distanceMillionKm: 0.39,
|
|
fuelCost: 50,
|
|
fuelBurnedFu: 0,
|
|
expectedPayout: 0,
|
|
seed: 1,
|
|
status: "in-flight",
|
|
resolvedAt: null,
|
|
});
|
|
|
|
describe("HQ screen", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
|
vi.setSystemTime(NOW);
|
|
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
|
|
});
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("shows idle and in-flight counts and counts down to the next arrival", async () => {
|
|
const state = samplePlayerState({
|
|
tutorial: null,
|
|
ships: [ship("a", "idle"), ship("b", "in-flight"), ship("c", "in-flight")],
|
|
activeDispatches: [dispatch("b", NOW + 125_000), dispatch("c", NOW + 65_000)],
|
|
});
|
|
renderApp(fakeApi({ getPlayerState: () => state }));
|
|
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
|
|
|
|
const statValue = (label: string) => screen.getByText(label).nextElementSibling;
|
|
expect(statValue("Ships idle")).toHaveTextContent("1");
|
|
expect(statValue("Ships in flight")).toHaveTextContent("2");
|
|
|
|
const next = screen.getByRole("heading", { name: "Next arrival" }).parentElement!;
|
|
expect(within(next).getByText("1m 05s")).toBeInTheDocument();
|
|
act(() => vi.advanceTimersByTime(10_000));
|
|
expect(within(next).getByText("55s")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows tutorial run progress toward the deal during the tutorial", async () => {
|
|
const state = samplePlayerState({
|
|
tutorial: {
|
|
loanerLocationId: "moon",
|
|
runInFlight: false,
|
|
runsCompleted: 3,
|
|
dealAvailable: false,
|
|
},
|
|
});
|
|
renderApp(fakeApi({ getPlayerState: () => state }));
|
|
expect(await screen.findByLabelText("Company runs")).toHaveTextContent(
|
|
`Company runs: 3 of ${content.config.tutorialRunsBeforeDeal}`,
|
|
);
|
|
expect(screen.queryByText("Loan from Halvorsen")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("loan on HQ", () => {
|
|
const total = content.dealLoanAmount;
|
|
const owner = (credits: number, loanBalance: number): PlayerState =>
|
|
samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits, loanBalance } });
|
|
|
|
it("shows the balance and a paid-off progress bar matching state", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => owner(2000, total * 0.75) }));
|
|
expect(await screen.findByLabelText("Loan balance")).toHaveTextContent(
|
|
`${(total * 0.75).toLocaleString("en-US")} cr left`,
|
|
);
|
|
expect(screen.getByRole("progressbar", { name: "Loan paid off" })).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"25",
|
|
);
|
|
});
|
|
|
|
it("pays down with the entered amount", async () => {
|
|
const api = fakeApi({
|
|
getPlayerState: () => owner(2000, 4000),
|
|
repayLoan: ({ amount }) => ({
|
|
company: { id: "c", name: "D", credits: 2000 - amount, loanBalance: 4000 - amount },
|
|
}),
|
|
});
|
|
renderApp(api);
|
|
await userEvent.type(await screen.findByLabelText("Pay down amount"), "750");
|
|
await userEvent.click(screen.getByRole("button", { name: "Pay down" }));
|
|
await waitFor(() => expect(api.call).toHaveBeenCalledWith("repayLoan", { amount: 750 }));
|
|
});
|
|
|
|
it("blocks an amount above the balance", async () => {
|
|
const api = fakeApi({ getPlayerState: () => owner(5000, 300) });
|
|
renderApp(api);
|
|
await userEvent.type(await screen.findByLabelText("Pay down amount"), "400");
|
|
expect(screen.getByRole("button", { name: "Pay down" })).toBeDisabled();
|
|
expect(screen.getByText("You only owe 300 cr.")).toBeInTheDocument();
|
|
expect(api.call).not.toHaveBeenCalledWith("repayLoan", expect.anything());
|
|
});
|
|
|
|
it("blocks an amount above the player's credits", async () => {
|
|
const api = fakeApi({ getPlayerState: () => owner(200, 3000) });
|
|
renderApp(api);
|
|
await userEvent.type(await screen.findByLabelText("Pay down amount"), "250");
|
|
expect(screen.getByRole("button", { name: "Pay down" })).toBeDisabled();
|
|
expect(screen.getByText("You only have 200 cr.")).toBeInTheDocument();
|
|
expect(api.call).not.toHaveBeenCalledWith("repayLoan", expect.anything());
|
|
});
|
|
|
|
it("is hidden once the loan is paid off", async () => {
|
|
renderApp(fakeApi({ getPlayerState: () => owner(2000, 0) }));
|
|
await screen.findByRole("heading", { level: 1, name: "Dashboard" });
|
|
expect(screen.queryByText("Loan from Halvorsen")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the lender's cut in a delivery summary", async () => {
|
|
const state: PlayerState = {
|
|
...owner(2000, 4000),
|
|
summaries: [
|
|
{
|
|
dispatchId: "d1",
|
|
shipId: "s1",
|
|
pilotId: "avatar",
|
|
originId: "earth",
|
|
destinationId: "moon",
|
|
tutorial: false,
|
|
credits: 538,
|
|
deadhead: false,
|
|
fuelCost: 0,
|
|
topUp: null,
|
|
goodId: "consumer-goods",
|
|
massTonnes: 12,
|
|
shipper: "Pacific Orbital Logistics",
|
|
loanRepayment: 230,
|
|
pilotPay: 0,
|
|
pilotXp: 10,
|
|
discoveredLocationId: null,
|
|
scriptedId: null,
|
|
restEndsAt: null,
|
|
resolvedAt: 1,
|
|
},
|
|
],
|
|
};
|
|
renderApp(
|
|
fakeApi({ getPlayerState: () => state, acknowledgeSummaries: () => ({ acknowledged: 1 }) }),
|
|
);
|
|
const dialog = await screen.findByRole("dialog", { name: "While you were away" });
|
|
expect(dialog).toHaveTextContent("230 cr went to the lender");
|
|
});
|
|
});
|