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 { 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

Game running here

; } 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( , ); 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( , ); expect(await screen.findByText("Game running here")).toBeInTheDocument(); }); });