4e0058d46c
- Unit: repoint moved screen imports (lib/screens, lib/game), mock $lib/app-nav (appScreen/activeView) instead of $app/navigation, drop the removed gameId props, assert screen/view selection. - e2e: add a dev-only window.__galaxyNav affordance; specs enter a game via enterGame(...) instead of a /games/:id URL; URL assertions become content assertions (the URL stays /game/); reload uses waitUntil:"commit" (shallow routing) and mocks /rpc on game entry. - Remove the obsolete report scroll-restore test (it relied on a SvelteKit route Snapshot that no longer exists); update the missing-membership test to the new lobby-redirect+toast behaviour. Fix a stale report.svelte docstring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
176 lines
6.4 KiB
TypeScript
176 lines
6.4 KiB
TypeScript
// Phase 12 end-to-end coverage for the order composer skeleton. The
|
|
// boot flow seeds an authenticated session and a draft directly
|
|
// through `/__debug/store`, then enters the game via the dev-only
|
|
// `window.__galaxyNav` affordance (the single-URL app-shell has no
|
|
// `/games/<id>/<view>` route) and exercises the order tab.
|
|
//
|
|
// The shell's per-game bootstrap now talks to the gateway on entry
|
|
// (lobby validation, report, order read-back). This spec does not
|
|
// stand up a real gateway, so those Connect-Web calls are aborted via
|
|
// `page.route` — the shell tolerates the failure (cache fallback +
|
|
// `failBootstrap`) and still renders the chrome. Aborting also keeps
|
|
// a mid-spec `page.reload()` from hanging: an unrouted `/rpc` call
|
|
// to a dead proxy never settles, which otherwise stalls the reload's
|
|
// load event.
|
|
//
|
|
// Persistence is covered by reloading the page mid-spec: the
|
|
// `OrderDraftStore` re-reads the same cache row on the next mount,
|
|
// so the rendered list survives the round-trip.
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
// `window.__galaxyDebug` is owned by `routes/__debug/store/+page.svelte`
|
|
// and `window.__galaxyNav` by `routes/+page.svelte`; both are typed by
|
|
// `tests/e2e/storage-keypair-persistence.spec.ts`.
|
|
|
|
const SESSION_ID = "phase-12-order-session";
|
|
const GAME_ID = "test-order";
|
|
|
|
// Fail-fast the shell's gateway calls so the spec needs no real
|
|
// backend and reloads settle promptly.
|
|
async function stubGateway(page: Page): Promise<void> {
|
|
await page.route("**/edge.v1.Gateway/**", (route) => route.abort());
|
|
}
|
|
|
|
// Load the app (seeded session → authenticated lobby) and enter the
|
|
// game on the map view through the in-memory nav affordance.
|
|
async function enterGameMap(page: Page): Promise<void> {
|
|
await page.goto("/");
|
|
await page.waitForFunction(() => window.__galaxyNav !== undefined);
|
|
await page.evaluate(
|
|
(id) => window.__galaxyNav!.enterGame(id, "map", {}),
|
|
GAME_ID,
|
|
);
|
|
await expect(page.getByTestId("game-shell")).toBeVisible();
|
|
await expect(page.getByTestId("active-view-map")).toBeVisible();
|
|
}
|
|
|
|
const SEED = [
|
|
{ kind: "placeholder" as const, id: "cmd-a", label: "first command" },
|
|
{ kind: "placeholder" as const, id: "cmd-b", label: "second command" },
|
|
{ kind: "placeholder" as const, id: "cmd-c", label: "third command" },
|
|
];
|
|
|
|
async function bootDebug(page: Page): Promise<void> {
|
|
await stubGateway(page);
|
|
await page.goto("/__debug/store");
|
|
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
|
|
await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
|
|
}
|
|
|
|
async function seedShell(page: Page): Promise<void> {
|
|
await bootDebug(page);
|
|
await page.evaluate(() => window.__galaxyDebug!.clearSession());
|
|
await page.evaluate(
|
|
(id) => window.__galaxyDebug!.setDeviceSessionId(id),
|
|
SESSION_ID,
|
|
);
|
|
await page.evaluate(
|
|
({ gameId, commands }) =>
|
|
window.__galaxyDebug!.clearOrderDraft(gameId).then(() =>
|
|
window.__galaxyDebug!.seedOrderDraft(gameId, commands),
|
|
),
|
|
{ gameId: GAME_ID, commands: SEED },
|
|
);
|
|
}
|
|
|
|
async function openOrderTool(page: Page, isMobile: boolean): Promise<void> {
|
|
if (isMobile) {
|
|
await page.getByTestId("bottom-tab-order").click();
|
|
} else {
|
|
await page.getByTestId("sidebar-tab-order").click();
|
|
}
|
|
await expect(page.getByTestId("sidebar-tool-order")).toBeVisible();
|
|
}
|
|
|
|
async function expectSeededRows(page: Page): Promise<void> {
|
|
const list = page.getByTestId("order-list");
|
|
await expect(list).toBeVisible();
|
|
for (let i = 0; i < SEED.length; i++) {
|
|
const row = page.getByTestId(`order-command-${i}`);
|
|
await expect(row).toBeVisible();
|
|
await expect(row.getByTestId(`order-command-label-${i}`)).toHaveText(
|
|
SEED[i]!.label,
|
|
);
|
|
}
|
|
await expect(page.getByTestId("order-empty")).toHaveCount(0);
|
|
}
|
|
|
|
test("seeded draft renders on the order tab and survives a reload", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const isMobile = testInfo.project.name.startsWith("chromium-mobile");
|
|
await seedShell(page);
|
|
await enterGameMap(page);
|
|
|
|
await openOrderTool(page, isMobile);
|
|
await expectSeededRows(page);
|
|
|
|
// Reload restores the `game` screen from the persisted nav snapshot,
|
|
// whose first authenticated render re-stamps screen history via
|
|
// SvelteKit shallow routing. That `pushState` lands right after the
|
|
// document loads and would abort a default `reload()` (which waits
|
|
// for `load`); waiting only for the navigation to commit sidesteps
|
|
// the race while still re-executing the app from scratch.
|
|
await page.reload({ waitUntil: "commit" });
|
|
await expect(page.getByTestId("game-shell")).toBeVisible();
|
|
await openOrderTool(page, isMobile);
|
|
await expectSeededRows(page);
|
|
});
|
|
|
|
test("removing a command from the order tab persists the removal", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const isMobile = testInfo.project.name.startsWith("chromium-mobile");
|
|
await seedShell(page);
|
|
await enterGameMap(page);
|
|
await openOrderTool(page, isMobile);
|
|
|
|
await expect(page.getByTestId("order-command-1")).toBeVisible();
|
|
await page.getByTestId("order-command-delete-1").click();
|
|
// The remaining two commands shift up by one slot.
|
|
await expect(page.getByTestId("order-command-label-0")).toHaveText(
|
|
SEED[0]!.label,
|
|
);
|
|
await expect(page.getByTestId("order-command-label-1")).toHaveText(
|
|
SEED[2]!.label,
|
|
);
|
|
await expect(page.getByTestId("order-command-2")).toHaveCount(0);
|
|
|
|
// See the note on the sibling test: the restored `game` screen
|
|
// re-stamps history on reload, so wait only for the navigation to
|
|
// commit to avoid the shallow-routing `pushState` aborting it.
|
|
await page.reload({ waitUntil: "commit" });
|
|
await expect(page.getByTestId("game-shell")).toBeVisible();
|
|
await openOrderTool(page, isMobile);
|
|
await expect(page.getByTestId("order-command-label-0")).toHaveText(
|
|
SEED[0]!.label,
|
|
);
|
|
await expect(page.getByTestId("order-command-label-1")).toHaveText(
|
|
SEED[2]!.label,
|
|
);
|
|
await expect(page.getByTestId("order-command-2")).toHaveCount(0);
|
|
});
|
|
|
|
test("empty draft renders the empty-state copy", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const isMobile = testInfo.project.name.startsWith("chromium-mobile");
|
|
await bootDebug(page);
|
|
await page.evaluate(() => window.__galaxyDebug!.clearSession());
|
|
await page.evaluate(
|
|
(id) => window.__galaxyDebug!.setDeviceSessionId(id),
|
|
SESSION_ID,
|
|
);
|
|
await page.evaluate(
|
|
(gameId) => window.__galaxyDebug!.clearOrderDraft(gameId),
|
|
GAME_ID,
|
|
);
|
|
|
|
await enterGameMap(page);
|
|
await openOrderTool(page, isMobile);
|
|
|
|
await expect(page.getByTestId("order-empty")).toBeVisible();
|
|
await expect(page.getByTestId("order-list")).toHaveCount(0);
|
|
});
|