deadhead-space-freight-tycoon/apps/web/test/on-device.test.tsx

94 lines
3.5 KiB
TypeScript

import {
DeviceClock,
GameEngine,
MemoryBackend,
SaveStorage,
TabLock,
type LockManagerLike,
} from "@deadhead/engine";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { ApiError } from "../src/api/client";
import { useApi } from "../src/api/context";
import { createEngineClient } from "../src/api/engine-client";
import { GameRoot } from "../src/GameRoot";
import type { Game } from "../src/platform/game";
function testGame(locks?: LockManagerLike): Game {
const storage = new SaveStorage(new MemoryBackend());
const clock = new DeviceClock(() => Date.parse("2100-01-01T00:00:00Z"));
const lock = new TabLock(locks);
const engine = new GameEngine({ storage, clock, canWrite: () => lock.held, devTools: false });
return { engine, lock, storage, clock, devTools: false };
}
describe("the client calls the engine (Step 14.7)", () => {
it("answers as the server did, refusals included", async () => {
const api = createEngineClient(testGame().engine);
await expect(api.call("getPlayerState", {})).rejects.toEqual(
new ApiError(401, "unauthorized", "There is no company on this device yet."),
);
const created = await api.call("createGuest", {});
expect(created.state.tutorial).not.toBeNull();
const refused = await api.call("acceptDeal", {}).catch((e: unknown) => e);
expect(refused).toBeInstanceOf(ApiError);
expect(refused).toMatchObject({ status: 409, code: "deal-not-available" });
});
});
/** Web Locks with one holder: `ifAvailable` gets nothing while it is held; `steal` takes it. */
class OneHolderLocks implements LockManagerLike {
#holder: { reject: (e: Error) => void } | null = null;
request(
_name: string,
options: { ifAvailable?: boolean; steal?: boolean },
callback: (lock: unknown) => unknown,
): Promise<unknown> {
if (this.#holder && options.ifAvailable) return Promise.resolve(callback(null));
if (this.#holder && options.steal) this.#holder.reject(new Error("stolen"));
return new Promise((resolve, reject) => {
this.#holder = { reject };
Promise.resolve(callback({})).then(resolve);
});
}
}
/** Shows that the engine is reachable through the API context. */
function Probe() {
useApi();
return <p>Game running here</p>;
}
describe("one tab runs the game (Step 14.7, decision 0.5.90)", () => {
it("shows 'open in another tab' until Use here takes the game over", async () => {
const locks = new OneHolderLocks();
// Another tab already has the game.
await new TabLock(locks).acquire();
render(
<QueryClientProvider client={new QueryClient()}>
<GameRoot game={testGame(locks)}>
<Probe />
</GameRoot>
</QueryClientProvider>,
);
expect(await screen.findByText("Deadhead is open in another tab.")).toBeInTheDocument();
expect(screen.queryByText("Game running here")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Use here" }));
expect(await screen.findByText("Game running here")).toBeInTheDocument();
});
it("runs the game at once when no other tab has it", async () => {
render(
<QueryClientProvider client={new QueryClient()}>
<GameRoot game={testGame(new OneHolderLocks())}>
<Probe />
</GameRoot>
</QueryClientProvider>,
);
expect(await screen.findByText("Game running here")).toBeInTheDocument();
});
});