189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import { describe, expect, it } from "vitest";
|
|
import { fullSave } from "../../schema/test/save-fixture";
|
|
import {
|
|
GameEngine,
|
|
MemoryBackend,
|
|
noNotifications,
|
|
NotificationSync,
|
|
plannedNotifications,
|
|
SaveStorage,
|
|
type NotificationScheduler,
|
|
type ScheduledNotification,
|
|
} from "../src/index";
|
|
import {
|
|
addPilot,
|
|
addShip,
|
|
makeOwner,
|
|
putBoard,
|
|
setCredits,
|
|
signUp,
|
|
TestClock,
|
|
testContract,
|
|
} from "./support";
|
|
|
|
/** The platform plugin, recording what is scheduled. */
|
|
class FakeScheduler implements NotificationScheduler {
|
|
scheduled: ScheduledNotification[] = [];
|
|
async replaceAll(list: ScheduledNotification[]) {
|
|
this.scheduled = list;
|
|
}
|
|
async requestPermission() {}
|
|
}
|
|
|
|
function setup(enabled = () => true) {
|
|
const clock = new TestClock();
|
|
const scheduler = new FakeScheduler();
|
|
const app = new GameEngine({
|
|
storage: new SaveStorage(new MemoryBackend()),
|
|
clock: { now: () => clock.now },
|
|
notifications: new NotificationSync(scheduler, enabled),
|
|
});
|
|
return { app, clock, scheduler };
|
|
}
|
|
|
|
/** An owner who dispatches the Courier on a board contract; returns the flight. */
|
|
async function dispatchOne(app: GameEngine) {
|
|
const guest = await signUp(app);
|
|
const { shipId } = await makeOwner(app, guest.avatarId);
|
|
await setCredits(app, 10_000);
|
|
const contract = testContract();
|
|
await putBoard(app, [contract]);
|
|
const { dispatch } = await app.call("createDispatch", {
|
|
kind: "contract",
|
|
contractId: contract.id,
|
|
shipId,
|
|
pilotId: guest.avatarId,
|
|
});
|
|
return dispatch;
|
|
}
|
|
|
|
describe("local notifications (Step 14.9, decision 0.5.91)", () => {
|
|
it("schedules one notification at a dispatch's arrival time", async () => {
|
|
const { app, scheduler } = setup();
|
|
const dispatch = await dispatchOne(app);
|
|
expect(scheduler.scheduled).toEqual([
|
|
{
|
|
id: 1,
|
|
at: dispatch.arrivalAt,
|
|
title: "Deadhead",
|
|
body: "Test Courier delivered to Moon",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("combines events in the same minute into one notification", () => {
|
|
const save = fullSave();
|
|
const landing = save.dispatches.find((d) => d.status === "in-flight")!;
|
|
// A second ship lands 20 seconds after the first, in the same minute.
|
|
const minuteStart = Math.floor(landing.arrivalAt / 60_000) * 60_000;
|
|
landing.arrivalAt = minuteStart + 10_000;
|
|
save.dispatches.push({
|
|
...structuredClone(landing),
|
|
id: "d2",
|
|
shipId: "s2",
|
|
arrivalAt: minuteStart + 30_000,
|
|
record: { ...landing.record, id: "d2", shipId: "s2", destinationId: "mars" },
|
|
});
|
|
const now = save.lastTimeSeen;
|
|
const planned = plannedNotifications(save, now);
|
|
const combined = planned.find((n) => n.at === minuteStart + 30_000)!;
|
|
expect(combined.title).toBe("Deadhead: 2 updates");
|
|
expect(combined.body.split("\n")).toEqual([
|
|
"Dusty Mule delivered to Moon",
|
|
"Swift Heron delivered to Mars",
|
|
]);
|
|
// The resting veteran and the shop job are separate minutes.
|
|
expect(planned.map((n) => n.at)).toEqual([...planned.map((n) => n.at)].sort((a, b) => a - b));
|
|
expect(planned.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("cancels everything when a new company starts", async () => {
|
|
const { app, scheduler } = setup();
|
|
await dispatchOne(app);
|
|
expect(scheduler.scheduled).toHaveLength(1);
|
|
await app.call("newCompany", {});
|
|
expect(scheduler.scheduled).toEqual([]);
|
|
});
|
|
|
|
it("schedules nothing with notifications turned off, and catches up when turned on", async () => {
|
|
let on = false;
|
|
const { app, scheduler } = setup(() => on);
|
|
await dispatchOne(app);
|
|
expect(scheduler.scheduled).toEqual([]);
|
|
on = true;
|
|
await app.resyncNotifications();
|
|
expect(scheduler.scheduled).toHaveLength(1);
|
|
});
|
|
|
|
it("does nothing on the web (no-op scheduler), without errors", async () => {
|
|
const app = new GameEngine({
|
|
storage: new SaveStorage(new MemoryBackend()),
|
|
clock: { now: () => new TestClock().now },
|
|
notifications: new NotificationSync(noNotifications),
|
|
});
|
|
await expect(dispatchOne(app)).resolves.toBeDefined();
|
|
await expect(app.requestNotificationPermission()).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("the sleep notification (Step 24.6, decision 0.5.139)", () => {
|
|
const SLEEP = "Your pilots are waiting for orders.";
|
|
const DAY = 24 * 60 * 60_000;
|
|
const sleepNotes = (list: ScheduledNotification[]) => list.filter((n) => n.body === SLEEP);
|
|
|
|
/** An owner with an Independent pilot and a Courier dispatched by hand. */
|
|
async function withIndependent(enabled = () => true) {
|
|
const test = setup(enabled);
|
|
const dispatch = await dispatchOne(test.app);
|
|
await setCredits(test.app, content.config.independentPilots.dispatchOfficePrice);
|
|
await test.app.call("buyDispatchOffice", {});
|
|
await addShip(test.app, { id: "s2", modelId: "courier", name: "Wren" });
|
|
await addPilot(test.app, { id: "p2", name: "Ada", assignedShipId: "s2" });
|
|
await test.app.call("setPilotMode", { pilotId: "p2", mode: "independent" });
|
|
return { ...test, dispatch };
|
|
}
|
|
|
|
it("schedules one at the last decision plus the threshold", async () => {
|
|
const { scheduler, clock } = await withIndependent();
|
|
expect(sleepNotes(scheduler.scheduled)).toEqual([
|
|
expect.objectContaining({ at: clock.now + DAY, title: "Deadhead", body: SLEEP }),
|
|
]);
|
|
});
|
|
|
|
it("moves it with the next manual decision, keeping just one", async () => {
|
|
const { app, scheduler, clock } = await withIndependent();
|
|
clock.now += 3 * 60 * 60_000;
|
|
await app.call("setRefuelPolicy", { shipId: "s2", policy: "auto" });
|
|
expect(sleepNotes(scheduler.scheduled)).toEqual([
|
|
expect.objectContaining({ at: clock.now + DAY }),
|
|
]);
|
|
});
|
|
|
|
it("schedules none without an Independent pilot, with notifications off, or for a new company", async () => {
|
|
const plain = setup();
|
|
await dispatchOne(plain.app);
|
|
expect(sleepNotes(plain.scheduler.scheduled)).toEqual([]);
|
|
|
|
let enabled = true;
|
|
const off = await withIndependent(() => enabled);
|
|
enabled = false;
|
|
await off.app.resyncNotifications();
|
|
expect(off.scheduler.scheduled).toEqual([]);
|
|
|
|
const fresh = await withIndependent();
|
|
await fresh.app.call("newCompany", {});
|
|
expect(sleepNotes(fresh.scheduler.scheduled)).toEqual([]);
|
|
});
|
|
|
|
it("drops it once the fleet is asleep", () => {
|
|
const save = fullSave();
|
|
save.pilots[0]!.mode = "independent";
|
|
save.company.lastManualDecisionAt = 0;
|
|
expect(sleepNotes(plannedNotifications(save, DAY + 1))).toEqual([]);
|
|
expect(sleepNotes(plannedNotifications(save, DAY - 1))).toEqual([
|
|
expect.objectContaining({ at: DAY }),
|
|
]);
|
|
});
|
|
});
|