feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game #62
@@ -9,6 +9,7 @@
|
|||||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||||
import { loadCore } from "../../platform/core/index";
|
import { loadCore } from "../../platform/core/index";
|
||||||
import { session } from "$lib/session-store.svelte";
|
import { session } from "$lib/session-store.svelte";
|
||||||
|
import { lobbyData } from "$lib/lobby-data.svelte";
|
||||||
|
|
||||||
const DEFAULT_MIN_PLAYERS = 2;
|
const DEFAULT_MIN_PLAYERS = 2;
|
||||||
const DEFAULT_MAX_PLAYERS = 8;
|
const DEFAULT_MAX_PLAYERS = 8;
|
||||||
@@ -102,9 +103,17 @@
|
|||||||
turnSchedule: trimmedSchedule,
|
turnSchedule: trimmedSchedule,
|
||||||
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
|
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
|
||||||
});
|
});
|
||||||
// Land on the private-games panel where the freshly created
|
// Refresh the lobby-data cache so the freshly-created game
|
||||||
// game shows up — the lobby-data store will refresh on next
|
// shows up in the private-games panel the moment we land
|
||||||
// mount.
|
// 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");
|
appScreen.go("games-private-games");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
formError = describeLobbyError(err);
|
formError = describeLobbyError(err);
|
||||||
|
|||||||
@@ -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.
|
// 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(
|
await expect(page.getByTestId("toast")).toContainText(
|
||||||
"this game is no longer available",
|
"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);
|
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,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await seedSession(page);
|
await seedSession(page);
|
||||||
// Seeded session → the dispatcher renders the lobby; the synthetic
|
// Seeded session → the dispatcher renders the lobby. F8-04b moved
|
||||||
// loader lives there behind the dev-affordances flag.
|
// 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.goto("/");
|
||||||
|
await page.waitForFunction(() => window.__galaxyNav !== undefined);
|
||||||
|
await page.evaluate(() => window.__galaxyNav!.go("synthetic-reports"));
|
||||||
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
|
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
|
||||||
|
|
||||||
const file = page.getByTestId("lobby-synthetic-file");
|
const file = page.getByTestId("lobby-synthetic-file");
|
||||||
|
|||||||
@@ -271,8 +271,11 @@ test.describe("Phase 8 — lobby flow", () => {
|
|||||||
// Default landing is `games-recruitment` (empty, no public games).
|
// Default landing is `games-recruitment` (empty, no public games).
|
||||||
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
|
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
|
||||||
|
|
||||||
// Paid tier exposes the `private games` sub-panel; navigate to it.
|
// Paid tier exposes the `private games` sub-panel; navigate to
|
||||||
await page.getByTestId("lobby-nav-games-private-games").click();
|
// 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 expect(page.getByTestId("lobby-games-private-empty")).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId("lobby-create-button").click();
|
await page.getByTestId("lobby-create-button").click();
|
||||||
@@ -348,14 +351,16 @@ test.describe("Phase 8 — lobby flow", () => {
|
|||||||
});
|
});
|
||||||
await completeLogin(page);
|
await completeLogin(page);
|
||||||
|
|
||||||
// Navigate to the invitations sub-panel.
|
// Navigate to the invitations sub-panel via the dev nav surface
|
||||||
await page.getByTestId("lobby-nav-games-invitations").click();
|
// (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 expect(page.getByTestId("lobby-invite-accept")).toBeVisible();
|
||||||
await page.getByTestId("lobby-invite-accept").click();
|
await page.getByTestId("lobby-invite-accept").click();
|
||||||
await expect(page.getByTestId("lobby-invite-accept")).toBeHidden();
|
await expect(page.getByTestId("lobby-invite-accept")).toBeHidden();
|
||||||
|
|
||||||
// Active-past now has the invited game.
|
// 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");
|
await expect(page.getByTestId("lobby-my-game-card")).toContainText("Invited Game");
|
||||||
expect(mocks.inviteRedeemCalls).toEqual([
|
expect(mocks.inviteRedeemCalls).toEqual([
|
||||||
{ gameId: "private-1", inviteId: "invite-1" },
|
{ gameId: "private-1", inviteId: "invite-1" },
|
||||||
|
|||||||
@@ -181,8 +181,10 @@ test.describe("F8-04b — tier gate", () => {
|
|||||||
await expect(privateGamesEntry).toBeVisible();
|
await expect(privateGamesEntry).toBeVisible();
|
||||||
|
|
||||||
// Free-tier callers reach the create form via the DEV-visible
|
// Free-tier callers reach the create form via the DEV-visible
|
||||||
// entry, but the backend still rejects the POST.
|
// entry, but the backend still rejects the POST. Use the dev
|
||||||
await privateGamesEntry.click();
|
// 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 page.getByTestId("lobby-create-button").click();
|
||||||
await expect(page.getByTestId("lobby-create-form")).toBeVisible();
|
await expect(page.getByTestId("lobby-create-form")).toBeVisible();
|
||||||
|
|
||||||
@@ -198,7 +200,7 @@ test.describe("F8-04b — tier gate", () => {
|
|||||||
});
|
});
|
||||||
await completeLogin(page);
|
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 page.getByTestId("lobby-create-button").click();
|
||||||
await expect(page.getByTestId("lobby-create-form")).toBeVisible();
|
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 });
|
const mocks = await mockGatewayTier(page, { isPaid: true });
|
||||||
await completeLogin(page);
|
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-games-private-empty")).toBeVisible();
|
||||||
await expect(page.getByTestId("lobby-create-button")).toBeVisible();
|
await expect(page.getByTestId("lobby-create-button")).toBeVisible();
|
||||||
await page.getByTestId("lobby-create-button").click();
|
await page.getByTestId("lobby-create-button").click();
|
||||||
|
|||||||
@@ -330,10 +330,10 @@ test.describe("F8-04 — profile screen", () => {
|
|||||||
await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
|
await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
|
||||||
|
|
||||||
// Navigate back to the games section. F8-04b replaced the bare
|
// Navigate back to the games section. F8-04b replaced the bare
|
||||||
// `Overview` page with a `games` parent + sub-panels; clicking
|
// `Overview` page with a `games` parent + sub-panels; the dev
|
||||||
// the parent resolves to the first visible sub-panel
|
// nav surface goes straight to the canonical landing
|
||||||
// (`recruitment` for a no-games session).
|
// (`games-recruitment`) so the assertion is viewport-agnostic.
|
||||||
await page.getByTestId("lobby-nav-games").click();
|
await page.evaluate(() => window.__galaxyNav!.go("games-recruitment"));
|
||||||
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
|
await expect(page.getByTestId("lobby-recruitment-empty")).toBeVisible();
|
||||||
await expect(page.getByTestId("lobby-account-name")).toContainText("Pilot");
|
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> {
|
async function loadSyntheticGame(page: Page): Promise<void> {
|
||||||
// Seeded session → the dispatcher renders the lobby; the synthetic
|
// Seeded session → the dispatcher renders the lobby. F8-04b moved
|
||||||
// loader lives there behind the dev-affordances flag.
|
// 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.goto("/");
|
||||||
|
await page.waitForFunction(() => window.__galaxyNav !== undefined);
|
||||||
|
await page.evaluate(() => window.__galaxyNav!.go("synthetic-reports"));
|
||||||
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
|
await expect(page.getByTestId("lobby-synthetic-section")).toBeVisible();
|
||||||
const file = page.getByTestId("lobby-synthetic-file");
|
const file = page.getByTestId("lobby-synthetic-file");
|
||||||
await file.setInputFiles({
|
await file.setInputFiles({
|
||||||
|
|||||||
Reference in New Issue
Block a user