test(ui): migrate suite to the app-shell (state-driven navigation)

- 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>
This commit is contained in:
Ilia Denisov
2026-05-23 20:49:35 +02:00
parent 80545e9f9d
commit 4e0058d46c
36 changed files with 707 additions and 343 deletions
+38 -7
View File
@@ -1,11 +1,14 @@
// Component tests for the Phase 8 lobby page. The lobby API and the
// Component tests for the Phase 8 lobby screen. The lobby API and the
// gateway client are mocked at module level; the session singleton is
// wired to a per-test `SessionStore`-backing IndexedDB so the page's
// boot path settles on `authenticated` and constructs a real
// GalaxyClient (which is then never called because the lobby API
// wrappers are stubs). The tests assert the section rendering, the
// inline race-name form for public games, and the invitation Accept
// flow.
// flow. The app-shell navigation store is mocked so opening a game
// (`activeView.reset()` + `appScreen.go("game", …)`) or the create
// form (`appScreen.go("lobby-create")`) never runs real `pushState`
// in JSDOM; the single-URL shell has no `/lobby`/`/games` routes.
import "fake-indexeddb/auto";
import { fireEvent, render, waitFor } from "@testing-library/svelte";
@@ -25,8 +28,19 @@ import { type GalaxyDB, openGalaxyDB } from "../src/platform/store/idb";
import { IDBCache } from "../src/platform/store/idb-cache";
import { WebCryptoKeyStore } from "../src/platform/store/webcrypto-keystore";
vi.mock("$app/navigation", () => ({
goto: vi.fn(async () => {}),
// The lobby screen navigates through the app-shell stores
// (`appScreen.go`, `activeView.reset`/`select`), which internally call
// SvelteKit `pushState`. Mock the whole nav module so the spies
// capture the transitions and no real history mutation runs in JSDOM.
const appScreenGoSpy = vi.fn();
const activeViewResetSpy = vi.fn();
const activeViewSelectSpy = vi.fn();
vi.mock("$lib/app-nav.svelte", () => ({
appScreen: { go: (...args: unknown[]) => appScreenGoSpy(...args) },
activeView: {
reset: (...args: unknown[]) => activeViewResetSpy(...args),
select: (...args: unknown[]) => activeViewSelectSpy(...args),
},
}));
const listMyGamesSpy = vi.fn();
@@ -105,6 +119,9 @@ beforeEach(async () => {
submitApplicationSpy.mockReset();
redeemInviteSpy.mockReset();
declineInviteSpy.mockReset();
appScreenGoSpy.mockReset();
activeViewResetSpy.mockReset();
activeViewSelectSpy.mockReset();
});
afterEach(async () => {
@@ -119,8 +136,10 @@ afterEach(async () => {
});
});
async function importLobbyPage(): Promise<typeof import("../src/routes/lobby/+page.svelte")> {
return import("../src/routes/lobby/+page.svelte");
async function importLobbyPage(): Promise<
typeof import("../src/lib/screens/lobby-screen.svelte")
> {
return import("../src/lib/screens/lobby-screen.svelte");
}
const baseDate = new Date("2026-05-07T10:00:00Z");
@@ -184,7 +203,7 @@ function makeApplication(id: string, status: string) {
};
}
describe("lobby page", () => {
describe("lobby screen", () => {
test("renders empty states for every section when API returns no items", async () => {
listMyGamesSpy.mockResolvedValue([]);
listPublicGamesSpy.mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
@@ -375,6 +394,18 @@ describe("lobby page", () => {
expect(disabledByLabel["Closed Run"]).toBe(false);
expect(disabledByLabel["Cancelled Run"]).toBe(true);
expect(disabledByLabel["Draft Run"]).toBe(true);
// Clicking a playable card resets the in-game view and enters the
// game screen with its id (the single-URL app-shell switches
// in-memory state instead of navigating to `/games/:id`).
const liveCard = cards.find(
(card) => card.querySelector("strong")?.textContent === "Live",
);
await fireEvent.click(liveCard!);
expect(activeViewResetSpy).toHaveBeenCalledTimes(1);
expect(appScreenGoSpy).toHaveBeenCalledWith("game", {
gameId: "g-running",
});
});
test("application status badges localise pending and approved states", async () => {