fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations
Two follow-up nits on the F8-04b sidebar: 1. The bare-`lobby` resolver (lobby-screen.svelte) redirected to `games-recruitment` unconditionally on mount. With games already in the player's roster the sidebar then highlighted the wrong sub-page. The resolver now awaits the lobby fan-out + account fetch, then hands off to the same `firstVisibleGamesScreen` helper the sidebar uses — so a fresh entry with games lands on `active-past`, the canonical-order fallback stays `recruitment`. 2. `games-invitations` was unconditionally visible in the sidebar. Now it follows the `active-past` rule: hidden until the pending-invites list reports >=1. The lobby shell's auto-kick effect treats it symmetrically — accepting / declining the last invite moves the player to the next visible sub-page once the fan-out has resolved. Acceptance order in games-invitations-screen.acceptInvite was also swapped to setMyGames-before-removeInvitation: both mutations land in the same microtask, so the new auto-kick sees the freshly added game in `myGames` when invitations drop to zero and routes the player to `active-past` instead of bouncing through `recruitment`. The visibility predicates and canonical order live in the new `src/lib/lobby-nav.ts` pure helper, shared between the sidebar and the resolver so they cannot disagree. Unit tests cover every combination of (hasMyGames, hasInvitations, isPaidOrDev). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// Pure-state coverage for the lobby `games` submenu visibility rules.
|
||||
// The same helper drives the sidebar's auto-expanded sub-item and the
|
||||
// bare-`lobby` resolver's redirect target; if they ever disagree the
|
||||
// player lands on the wrong sub-page on entry.
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
firstVisibleGamesScreen,
|
||||
visibleGamesSubScreens,
|
||||
type LobbyNavState,
|
||||
} from "../src/lib/lobby-nav";
|
||||
|
||||
function state(overrides: Partial<LobbyNavState> = {}): LobbyNavState {
|
||||
return {
|
||||
hasMyGames: false,
|
||||
hasInvitations: false,
|
||||
isPaidOrDev: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("visibleGamesSubScreens", () => {
|
||||
test("free-tier player with nothing — only recruitment is visible", () => {
|
||||
expect(visibleGamesSubScreens(state())).toEqual(["games-recruitment"]);
|
||||
});
|
||||
|
||||
test("player with games — active-past leads recruitment", () => {
|
||||
expect(visibleGamesSubScreens(state({ hasMyGames: true }))).toEqual([
|
||||
"games-active-past",
|
||||
"games-recruitment",
|
||||
]);
|
||||
});
|
||||
|
||||
test("player with pending invites — invitations appears after recruitment", () => {
|
||||
expect(visibleGamesSubScreens(state({ hasInvitations: true }))).toEqual([
|
||||
"games-recruitment",
|
||||
"games-invitations",
|
||||
]);
|
||||
});
|
||||
|
||||
test("paid / DEV tier — private-games tails the list", () => {
|
||||
expect(visibleGamesSubScreens(state({ isPaidOrDev: true }))).toEqual([
|
||||
"games-recruitment",
|
||||
"games-private-games",
|
||||
]);
|
||||
});
|
||||
|
||||
test("all four flags on — full canonical order", () => {
|
||||
expect(
|
||||
visibleGamesSubScreens(
|
||||
state({
|
||||
hasMyGames: true,
|
||||
hasInvitations: true,
|
||||
isPaidOrDev: true,
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
"games-active-past",
|
||||
"games-recruitment",
|
||||
"games-invitations",
|
||||
"games-private-games",
|
||||
]);
|
||||
});
|
||||
|
||||
test("recruitment is always present regardless of input", () => {
|
||||
const variants: LobbyNavState[] = [
|
||||
state(),
|
||||
state({ hasMyGames: true }),
|
||||
state({ hasInvitations: true }),
|
||||
state({ isPaidOrDev: true }),
|
||||
state({ hasMyGames: true, hasInvitations: true, isPaidOrDev: true }),
|
||||
];
|
||||
for (const v of variants) {
|
||||
expect(visibleGamesSubScreens(v)).toContain("games-recruitment");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("firstVisibleGamesScreen", () => {
|
||||
test("free-tier nothing → recruitment", () => {
|
||||
expect(firstVisibleGamesScreen(state())).toBe("games-recruitment");
|
||||
});
|
||||
|
||||
test("player with games → active-past wins", () => {
|
||||
expect(firstVisibleGamesScreen(state({ hasMyGames: true }))).toBe(
|
||||
"games-active-past",
|
||||
);
|
||||
});
|
||||
|
||||
test("invitee-only (no games yet) → recruitment still wins over invitations by canonical order", () => {
|
||||
// Pending invites do NOT promote `invitations` above `recruitment`;
|
||||
// the canonical order places `recruitment` second only to
|
||||
// `active-past`. This guards against accidentally re-ordering the
|
||||
// sub-items in a way that surprises invitee-only sessions.
|
||||
expect(firstVisibleGamesScreen(state({ hasInvitations: true }))).toBe(
|
||||
"games-recruitment",
|
||||
);
|
||||
});
|
||||
|
||||
test("games + invites → active-past still wins", () => {
|
||||
expect(
|
||||
firstVisibleGamesScreen(
|
||||
state({ hasMyGames: true, hasInvitations: true }),
|
||||
),
|
||||
).toBe("games-active-past");
|
||||
});
|
||||
|
||||
test("paid tier with no games → recruitment, not private-games", () => {
|
||||
expect(firstVisibleGamesScreen(state({ isPaidOrDev: true }))).toBe(
|
||||
"games-recruitment",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user