feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game #62

Merged
developer merged 4 commits from feature/f8-04b-lobby-restructure into development 2026-05-27 07:18:33 +00:00
7 changed files with 48 additions and 21 deletions
Showing only changes of commit cff7cc3859 - Show all commits
@@ -9,6 +9,7 @@
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { loadCore } from "../../platform/core/index";
import { session } from "$lib/session-store.svelte";
import { lobbyData } from "$lib/lobby-data.svelte";
const DEFAULT_MIN_PLAYERS = 2;
const DEFAULT_MAX_PLAYERS = 8;
@@ -102,9 +103,17 @@
turnSchedule: trimmedSchedule,
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
});
// Land on the private-games panel where the freshly created
// game shows up the lobby-data store will refresh on next
// mount.
// Refresh the lobby-data cache so the freshly-created game
// shows up in the private-games panel the moment we land
// there. The shared store is otherwise lazy (ensure-on-
// first-mount) and would render a stale list across the
// navigation.
try {
await lobbyData.refresh();
} catch {
// Refresh failure does not block the navigation —
// the panel will retry on its own mount.
}
appScreen.go("games-private-games");
} catch (err) {
formError = describeLobbyError(err);
+4 -1
View File
@@ -250,9 +250,12 @@ test("missing-membership game drops back to the lobby with an unavailable toast"
);
// Back on the lobby (game shell unmounted), with the unavailable toast.
// F8-04b moved the `create new game` button into the `private games`
// sub-panel; the always-present lobby chrome signal is the identity
// strip, so assert that instead.
await expect(page.getByTestId("toast")).toContainText(
"this game is no longer available",
);
await expect(page.getByTestId("lobby-create-button")).toBeVisible();
await expect(page.getByTestId("lobby-account-name")).toBeVisible();
await expect(page.getByTestId("game-shell")).toHaveCount(0);
});
@@ -136,9 +136,13 @@ test("synthetic-report loader navigates from lobby to map and renders", async ({
page,
}) => {
await seedSession(page);
// Seeded session → the dispatcher renders the lobby; the synthetic
// loader lives there behind the dev-affordances flag.
// Seeded session → the dispatcher renders the lobby. F8-04b moved
// the synthetic-report loader off of Overview into its own
// dev-only top-level screen; jump straight to it via the dev nav
// surface so the spec is viewport-agnostic.
await page.goto("/");
await page.waitForFunction(() => window.__galaxyNav !== undefined);
await page.evaluate(() => window.__galaxyNav!.go("synthetic-reports"));
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
const file = page.getByTestId("lobby-synthetic-file");
+10 -5
View File
@@ -271,8 +271,11 @@ test.describe("Phase 8 — lobby flow", () => {
// Default landing is `games-recruitment` (empty, no public games).
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
// Paid tier exposes the `private games` sub-panel; navigate to it.
await page.getByTestId("lobby-nav-games-private-games").click();
// Paid tier exposes the `private games` sub-panel; navigate to
// it via the dev nav surface so the spec is viewport-agnostic
// (the mobile sidebar hides the desktop submenu behind a
// dropdown — that UX is covered by lobby-submenu specs).
await page.evaluate(() => window.__galaxyNav!.go("games-private-games"));
await expect(page.getByTestId("lobby-games-private-empty")).toBeVisible();
await page.getByTestId("lobby-create-button").click();
@@ -348,14 +351,16 @@ test.describe("Phase 8 — lobby flow", () => {
});
await completeLogin(page);
// Navigate to the invitations sub-panel.
await page.getByTestId("lobby-nav-games-invitations").click();
// Navigate to the invitations sub-panel via the dev nav surface
// (viewport-agnostic — the mobile sidebar hides the desktop
// submenu behind a dropdown).
await page.evaluate(() => window.__galaxyNav!.go("games-invitations"));
await expect(page.getByTestId("lobby-invite-accept")).toBeVisible();
await page.getByTestId("lobby-invite-accept").click();
await expect(page.getByTestId("lobby-invite-accept")).toBeHidden();
// Active-past now has the invited game.
await page.getByTestId("lobby-nav-games-active-past").click();
await page.evaluate(() => window.__galaxyNav!.go("games-active-past"));
await expect(page.getByTestId("lobby-my-game-card")).toContainText("Invited Game");
expect(mocks.inviteRedeemCalls).toEqual([
{ gameId: "private-1", inviteId: "invite-1" },
@@ -181,8 +181,10 @@ test.describe("F8-04b — tier gate", () => {
await expect(privateGamesEntry).toBeVisible();
// Free-tier callers reach the create form via the DEV-visible
// entry, but the backend still rejects the POST.
await privateGamesEntry.click();
// entry, but the backend still rejects the POST. Use the dev
// nav surface so the assertion works on mobile too (the mobile
// sidebar tucks the private-games entry behind a dropdown).
await page.evaluate(() => window.__galaxyNav!.go("games-private-games"));
await page.getByTestId("lobby-create-button").click();
await expect(page.getByTestId("lobby-create-form")).toBeVisible();
@@ -198,7 +200,7 @@ test.describe("F8-04b — tier gate", () => {
});
await completeLogin(page);
await page.getByTestId("lobby-nav-games-private-games").click();
await page.evaluate(() => window.__galaxyNav!.go("games-private-games"));
await page.getByTestId("lobby-create-button").click();
await expect(page.getByTestId("lobby-create-form")).toBeVisible();
@@ -227,7 +229,7 @@ test.describe("F8-04b — tier gate", () => {
const mocks = await mockGatewayTier(page, { isPaid: true });
await completeLogin(page);
await page.getByTestId("lobby-nav-games-private-games").click();
await page.evaluate(() => window.__galaxyNav!.go("games-private-games"));
await expect(page.getByTestId("lobby-games-private-empty")).toBeVisible();
await expect(page.getByTestId("lobby-create-button")).toBeVisible();
await page.getByTestId("lobby-create-button").click();
+4 -4
View File
@@ -330,10 +330,10 @@ test.describe("F8-04 — profile screen", () => {
await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
// Navigate back to the games section. F8-04b replaced the bare
// `Overview` page with a `games` parent + sub-panels; clicking
// the parent resolves to the first visible sub-panel
// (`recruitment` for a no-games session).
await page.getByTestId("lobby-nav-games").click();
// `Overview` page with a `games` parent + sub-panels; the dev
// nav surface goes straight to the canonical landing
// (`games-recruitment`) so the assertion is viewport-agnostic.
await page.evaluate(() => window.__galaxyNav!.go("games-recruitment"));
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
@@ -135,9 +135,13 @@ async function bootSession(page: Page): Promise<void> {
}
async function loadSyntheticGame(page: Page): Promise<void> {
// Seeded session → the dispatcher renders the lobby; the synthetic
// loader lives there behind the dev-affordances flag.
// Seeded session → the dispatcher renders the lobby. F8-04b moved
// the synthetic-report loader off of Overview into its own
// dev-only top-level screen; jump straight to it via the dev nav
// surface so the spec is viewport-agnostic.
await page.goto("/");
await page.waitForFunction(() => window.__galaxyNav !== undefined);
await page.evaluate(() => window.__galaxyNav!.go("synthetic-reports"));
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
const file = page.getByTestId("lobby-synthetic-file");
await file.setInputFiles({