94 lines
3.5 KiB
TypeScript
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();
|
|
});
|
|
});
|