deadhead-space-freight-tycoon/apps/web/test/jobs-table.test.tsx

127 lines
4.2 KiB
TypeScript

import type { Contract, Ship } from "@deadhead/schema";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it } from "vitest";
import { usePreferences } from "../src/state/preferences";
import { pairOptions } from "../src/screens/DispatchSheet";
import { jobFigures } from "../src/screens/jobFigures";
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
const NOW = Date.now();
const job = (overrides: Partial<Contract>): Contract =>
({
id: "j1",
locationId: "earth",
originId: "earth",
destinationId: "moon",
goodId: "consumer-goods",
cargoTypeId: "standard",
massTonnes: 10,
payout: 2000,
shipper: "Pacific Orbital Logistics",
offeredAt: NOW,
expiresAt: NOW + 3_600_000,
priority: false,
deliveryWindowGameHours: null,
tier: null,
requiredSkill: null,
requiredLongHaulLevel: 0,
firstContact: false,
...overrides,
}) as Contract;
const ship = (id: string, overrides: Partial<Ship> = {}): Ship => ({
id,
modelId: "courier",
name: id,
status: "idle",
locationId: "earth",
dispatchId: null,
lastArrivalAt: 0,
...shipExtras("courier"),
pilotId: "avatar",
...overrides,
});
function owner(ships: Ship[]) {
const base = samplePlayerState();
return samplePlayerState({
tutorial: null,
bays: [{ id: "b", locationId: "earth" }],
ships,
pilots: [{ ...base.pilots[0]!, assignedShipId: ships[0]?.id ?? null }],
});
}
function show(contracts: Contract[], ships: Ship[]) {
renderApp(
fakeApi({
getPlayerState: () => owner(ships),
getContractBoard: () => ({ contracts, periodEndsAt: NOW + 3_600_000 }),
}),
"/contracts?location=earth",
);
}
const rowNames = () =>
screen
.getAllByRole("row")
.map((r) => r.getAttribute("aria-label"))
.filter(Boolean);
describe("jobs table (decision 0.5.169)", () => {
beforeEach(() => usePreferences.setState({ jobSort: "net" }));
it("shows the same net as the dispatch sheet", () => {
const state = owner([ship("s1")]);
const contract = job({});
const figures = jobFigures(state, contract, NOW);
expect(figures.ready).toBe(true);
expect(figures.estimate).toBe(false);
expect(figures.net).toBe(pairOptions(state, contract, NOW)[0]!.netProfit);
expect(figures.perHour).toBe(Math.round(figures.net! / figures.hours!));
});
it("sorts by net, then by credits per hour, and remembers the choice", async () => {
const big = job({
id: "big",
goodId: "packaged-food",
payout: 3000,
destinationId: "moon",
massTonnes: 18,
});
const small = job({ id: "small", goodId: "mail", payout: 2000, massTonnes: 1 });
show([small, big], [ship("s1")]);
await screen.findByRole("table");
expect(rowNames()).toEqual(["Packaged Food", "Mail & Parcels"]);
const net = screen.getByRole("columnheader", { name: /Net/ });
expect(net).toHaveAttribute("aria-sort", "descending");
await userEvent.click(within(screen.getByRole("table")).getByRole("button", { name: /cr\/h/ }));
expect(screen.getByRole("columnheader", { name: /cr\/h/ })).toHaveAttribute(
"aria-sort",
"descending",
);
expect(net).toHaveAttribute("aria-sort", "none");
expect(usePreferences.getState().jobSort).toBe("rate");
});
it("keeps jobs it can't take below, with the reason and an estimate", async () => {
// The only ship is away: the figures are an estimate, and the row says why.
const away = ship("s1", { status: "in-flight", locationId: null, dispatchId: "d" });
show([job({})], [away]);
await screen.findByRole("table");
expect(screen.getByText("Can't take yet")).toBeInTheDocument();
const row = screen.getByRole("row", { name: "Consumer Goods" });
expect(row).toHaveTextContent("No idle ship at Earth");
expect(within(row).getByLabelText("Net, estimated")).toHaveTextContent(/^≈/);
});
it("opens the job from its row", async () => {
show([job({})], [ship("s1")]);
await userEvent.click(await screen.findByRole("button", { name: "Dispatch" }));
await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
});
});