fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s

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:
Ilia Denisov
2026-05-27 10:17:57 +02:00
parent 8e8b34d112
commit 6fbab5417f
6 changed files with 307 additions and 85 deletions
+114
View File
@@ -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",
);
});
});