119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine.
|
|
import { content } from "@deadhead/content";
|
|
import { contractBoardResponseSchema, errorResponseSchema } from "@deadhead/schema";
|
|
import { boardPeriodMs, createDispatchRecord, type Contract } from "@deadhead/sim";
|
|
import { describe, expect, it } from "vitest";
|
|
import { useTestApp, signUp, call, edit, readSave, makeOwner, type Guest } from "../support";
|
|
const randomUUID = () => globalThis.crypto.randomUUID();
|
|
|
|
const testApp = useTestApp();
|
|
|
|
async function board(guest: Guest) {
|
|
const response = await call(testApp().app, guest, "GET", "/api/tutorial/board");
|
|
expect(response.statusCode).toBe(200);
|
|
return contractBoardResponseSchema.parse(response.json());
|
|
}
|
|
|
|
/** Records a contract as accepted, with the tutorial dispatch it would create. */
|
|
async function accept(guest: Guest, contract: Contract, now: number) {
|
|
const id = randomUUID();
|
|
const record = createDispatchRecord(
|
|
{
|
|
id,
|
|
ship: { id: "loaner", modelId: "courier", locationId: contract.originId, inFlight: false },
|
|
pilot: {
|
|
id: guest.avatarId,
|
|
isAvatar: true,
|
|
assignedShipId: null,
|
|
inFlight: false,
|
|
skills: {},
|
|
restEndsAt: null,
|
|
},
|
|
contract,
|
|
tutorial: true,
|
|
now,
|
|
worldSeed: 1,
|
|
},
|
|
content,
|
|
);
|
|
await edit(testApp().app, (save) => {
|
|
save.dispatches.push({
|
|
id,
|
|
shipId: null,
|
|
independent: false,
|
|
pilotId: guest.avatarId,
|
|
tutorial: true,
|
|
record,
|
|
arrivalAt: record.arrivalAt,
|
|
status: "in-flight",
|
|
resolution: null,
|
|
resolvedAt: null,
|
|
acknowledgedAt: null,
|
|
event: null,
|
|
eventApplied: null,
|
|
});
|
|
save.acceptedContracts.push({
|
|
contractId: contract.id,
|
|
periodIndex: 0,
|
|
dispatchId: id,
|
|
acceptedAt: now,
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("tutorial board endpoint", () => {
|
|
it("gives a tutorial player at Earth a full board of Earth→Moon contracts", async () => {
|
|
const { app, clock } = testApp();
|
|
const guest = await signUp(app);
|
|
const { contracts, periodEndsAt } = await board(guest);
|
|
|
|
expect(contracts).toHaveLength(content.config.tutorialBoardSize);
|
|
for (const c of contracts) {
|
|
expect([c.originId, c.destinationId]).toEqual(["earth", "moon"]);
|
|
expect(c.cargoTypeId).toBe("standard");
|
|
}
|
|
expect(periodEndsAt).toBeGreaterThan(clock.now);
|
|
expect(periodEndsAt - clock.now).toBeLessThanOrEqual(boardPeriodMs(content.config));
|
|
});
|
|
|
|
it("offers Moon→Earth when the loaner is at the Moon", async () => {
|
|
const { app } = testApp();
|
|
const guest = await signUp(app);
|
|
await edit(app, (save) => {
|
|
save.player.loanerLocationId = "moon";
|
|
});
|
|
for (const c of (await board(guest)).contracts) {
|
|
expect([c.originId, c.destinationId]).toEqual(["moon", "earth"]);
|
|
}
|
|
});
|
|
|
|
it("stores the board: repeated requests in a period return the same contracts", async () => {
|
|
const { clock } = testApp();
|
|
const guest = await signUp(testApp().app);
|
|
const first = await board(guest);
|
|
clock.advance(60_000);
|
|
expect((await board(guest)).contracts).toEqual(first.contracts);
|
|
expect((await readSave(testApp().app)).contractBoards).toHaveLength(1);
|
|
});
|
|
|
|
it("hides contracts the player has already accepted", async () => {
|
|
const { clock } = testApp();
|
|
const guest = await signUp(testApp().app);
|
|
const before = await board(guest);
|
|
await accept(guest, before.contracts[0]!, clock.now);
|
|
|
|
const after = await board(guest);
|
|
expect(after.contracts).toHaveLength(before.contracts.length - 1);
|
|
expect(after.contracts.map((c) => c.id)).not.toContain(before.contracts[0]!.id);
|
|
});
|
|
|
|
it("refuses players who are past the tutorial", async () => {
|
|
const { app } = testApp();
|
|
const guest = await signUp(app);
|
|
await makeOwner(testApp().app, guest.avatarId);
|
|
const response = await call(app, guest, "GET", "/api/tutorial/board");
|
|
expect(response.statusCode).toBe(409);
|
|
expect(errorResponseSchema.parse(response.json()).error.code).toBe("not-in-tutorial");
|
|
});
|
|
});
|