ui/phase-14: auto-sync order draft + always GET on boot + header headline
Replaces the manual Submit button with an auto-sync pipeline driven by `OrderDraftStore`: every successful add / remove / move coalesces a `submitOrder` call so the engine always mirrors the local draft. Removing the last command sends an empty cmd[] PUT — the engine, repo, and rest model now accept that as a valid "player cleared their draft" state. `hydrateFromServer` is now invoked unconditionally on game boot so a fresh device picks up the player's stored order, and the local cache is overwritten by the server's view (server is the source of truth). Header replaces the static "race ?" + turn counter with a single headline string `<race> @ <game>, turn <n>`, sourced from the engine's Report.race + the lobby's GameSummary.gameName + the live turn number, with a `?` fallback while any piece is loading. Tests: - engine: empty PUT round-trips, repo round-trips empty Commands - order-draft: auto-sync sends full draft on every mutation, rejected response surfaces error sync status, rapid mutations coalesce, server hydration overwrites cache - order-tab: per-row status flips through the auto-sync lifecycle, remove → empty cmd[] PUT, rejected → retry button - inspector overlay: applied + valid + submitting all participate in the optimistic projection - header: live race / game / turn rendering with fall-back Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
// Component tests for the Phase 10 in-game shell header. The header
|
||||
// composes the static `race ?` placeholder, the placeholder
|
||||
// turn-counter (Phase 11 wires the live source), the view-menu, and
|
||||
// the account-menu. The tests assert the placeholder copy, that
|
||||
// every view-menu entry dispatches `goto` with the right URL, and
|
||||
// that the Logout entry of the account-menu calls
|
||||
// `session.signOut("user")`.
|
||||
// Component tests for the in-game shell header. The header composes
|
||||
// the headline strip (`<race> @ <game>, turn N`, falling back to `?`
|
||||
// while the lobby / report calls are in flight), the view-menu, and
|
||||
// the account-menu. The tests assert the headline copy, that every
|
||||
// view-menu entry dispatches `goto` with the right URL, and that the
|
||||
// Logout entry of the account-menu calls `session.signOut("user")`.
|
||||
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { fireEvent, render } from "@testing-library/svelte";
|
||||
@@ -20,6 +19,31 @@ import {
|
||||
import { i18n } from "../src/lib/i18n/index.svelte";
|
||||
import { session } from "../src/lib/session-store.svelte";
|
||||
import Header from "../src/lib/header/header.svelte";
|
||||
import {
|
||||
GAME_STATE_CONTEXT_KEY,
|
||||
GameStateStore,
|
||||
} from "../src/lib/game-state.svelte";
|
||||
|
||||
function withGameState(opts: {
|
||||
gameName?: string;
|
||||
race?: string;
|
||||
turn?: number;
|
||||
} = {}): Map<unknown, unknown> {
|
||||
const store = new GameStateStore();
|
||||
store.gameName = opts.gameName ?? "";
|
||||
if (opts.race !== undefined || opts.turn !== undefined) {
|
||||
store.report = {
|
||||
turn: opts.turn ?? 0,
|
||||
mapWidth: 1000,
|
||||
mapHeight: 1000,
|
||||
planetCount: 0,
|
||||
planets: [],
|
||||
race: opts.race ?? "",
|
||||
};
|
||||
store.status = "ready";
|
||||
}
|
||||
return new Map<unknown, unknown>([[GAME_STATE_CONTEXT_KEY, store]]);
|
||||
}
|
||||
|
||||
const gotoSpy = vi.fn(async (..._args: unknown[]) => {});
|
||||
vi.mock("$app/navigation", () => ({
|
||||
@@ -37,19 +61,43 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("game-shell header", () => {
|
||||
test("renders the static race / turn placeholders and toggles", () => {
|
||||
test("renders fall-back placeholders before the lobby / report data lands", () => {
|
||||
const onToggleSidebar = vi.fn();
|
||||
const ui = render(Header, {
|
||||
props: { gameId: "g1", sidebarOpen: false, onToggleSidebar },
|
||||
context: withGameState(),
|
||||
});
|
||||
expect(ui.getByTestId("race-name")).toHaveTextContent("race ?");
|
||||
expect(ui.getByTestId("turn-counter").textContent ?? "").toMatch(
|
||||
/turn\s+\?/,
|
||||
expect(ui.getByTestId("game-shell-headline")).toHaveTextContent(
|
||||
"? @ ?, turn ?",
|
||||
);
|
||||
expect(ui.getByTestId("view-menu-trigger")).toBeInTheDocument();
|
||||
expect(ui.getByTestId("account-menu-trigger")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders the live race / game / turn from GameStateStore", () => {
|
||||
const ui = render(Header, {
|
||||
props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} },
|
||||
context: withGameState({
|
||||
gameName: "Phase 14",
|
||||
race: "Federation",
|
||||
turn: 7,
|
||||
}),
|
||||
});
|
||||
expect(ui.getByTestId("game-shell-headline")).toHaveTextContent(
|
||||
"Federation @ Phase 14, turn 7",
|
||||
);
|
||||
});
|
||||
|
||||
test("partial data still falls back gracefully (race known, game unknown)", () => {
|
||||
const ui = render(Header, {
|
||||
props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} },
|
||||
context: withGameState({ race: "Federation", turn: 3 }),
|
||||
});
|
||||
expect(ui.getByTestId("game-shell-headline")).toHaveTextContent(
|
||||
"Federation @ ?, turn 3",
|
||||
);
|
||||
});
|
||||
|
||||
test("clicking the sidebar toggle invokes the prop callback", async () => {
|
||||
const onToggleSidebar = vi.fn();
|
||||
const ui = render(Header, {
|
||||
|
||||
Reference in New Issue
Block a user