From 6bead30b4ee166c728f80668bcda149505e90bae Mon Sep 17 00:00:00 2001 From: Mohannad Ehab <157999295+MohanEhab@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:28:16 +0300 Subject: [PATCH 1/3] fix(module-06-lobby-matchmaking): resolve Active Lobby link from real membership The sidebar's Active Lobby link pointed at a hardcoded lobby code (SP-7F-29). Resolve the user's active lobby from lobbyApi.getMyActive() and hide the link when they are not in one. Also clear the stored lobby credential on leave and add the test:coverage script. --- package.json | 1 + src/__tests__/LobbyPage.test.tsx | 19 ++++++++++++++++ src/__tests__/SidebarFriendBadge.test.tsx | 15 +++++++++++++ src/components/layout/Sidebar.tsx | 22 ++++++++++++++++--- src/components/lobby/CreateLobbyModal.tsx | 20 ++++++++--------- src/features/lobby/LobbyPage.tsx | 10 +++++++-- tests/e2e/module-06-lobby-matchmaking.spec.ts | 3 +-- 7 files changed, 73 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index e872560..c107131 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "eslint", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "test:e2e": "playwright test" }, diff --git a/src/__tests__/LobbyPage.test.tsx b/src/__tests__/LobbyPage.test.tsx index e298e01..98b5010 100644 --- a/src/__tests__/LobbyPage.test.tsx +++ b/src/__tests__/LobbyPage.test.tsx @@ -22,6 +22,7 @@ vi.stubGlobal('fetch', mockFetch); beforeEach(() => { mockFetch.mockReset(); mockPush.mockReset(); + sessionStorage.clear(); }); function identity(userId: string, name: string) { @@ -63,12 +64,30 @@ async function renderPage(lobbyId = 'lobby-1') { describe('LobbyPage', () => { it('renders a not-found state on a 404', async () => { + sessionStorage.setItem('lobby-credential:lobby-1', JSON.stringify({ code: 'ABCD', linkToken: 'secret' })); setupFetch(u => { if (u.includes('/api/lobbies/lobby-1') && !u.includes('capabilities')) return jsonResponse(404, { error: { code: 'Lobbies.NotFound', message: 'Not found' } }); return null; }); await renderPage(); await waitFor(() => expect(screen.getByText('Lobby not found.')).toBeInTheDocument()); + expect(sessionStorage.getItem('lobby-credential:lobby-1')).toBeNull(); + }); + + it('clears the revealed credential when leaving a lobby', async () => { + sessionStorage.setItem('lobby-credential:lobby-1', JSON.stringify({ code: 'ABCD', linkToken: 'secret' })); + setupFetch((u, opts) => { + if (u.endsWith('/api/lobbies/lobby-1') && (opts?.method ?? 'GET') === 'GET') { + return jsonResponse(200, lobby({ hostUserId: 'u-1', seats: [seat('u-1', 'Me', { isHost: true })], allowedActions: ['leave'] })); + } + if (u.endsWith('/api/lobbies/lobby-1/leave') && opts?.method === 'POST') return jsonResponse(200, {}); + return null; + }); + await renderPage(); + const leaveButton = await screen.findByRole('button', { name: 'Leave' }); + await act(async () => { fireEvent.click(leaveButton); }); + await waitFor(() => expect(mockPush).toHaveBeenCalledWith('/dashboard')); + expect(sessionStorage.getItem('lobby-credential:lobby-1')).toBeNull(); }); it('renders seats and the Ready toggle for a joined member', async () => { diff --git a/src/__tests__/SidebarFriendBadge.test.tsx b/src/__tests__/SidebarFriendBadge.test.tsx index 2b85c17..2605507 100644 --- a/src/__tests__/SidebarFriendBadge.test.tsx +++ b/src/__tests__/SidebarFriendBadge.test.tsx @@ -32,12 +32,19 @@ vi.mock('@/features/friends/FriendSummaryContext', () => ({ useFriendSummary: () => mockSummaryState, })); +const mockGetMyActive = vi.fn(() => Promise.resolve({ lobby: null, ticketId: null })); +vi.mock('@/features/lobby/lobbyApi', () => ({ + lobbyApi: { getMyActive: () => mockGetMyActive() }, +})); + // ── Tests ──────────────────────────────────────────────────────────────────── beforeEach(() => { mockSummaryState.summary = null; mockSummaryState.loading = false; mockSummaryState.error = null; + mockGetMyActive.mockReset(); + mockGetMyActive.mockResolvedValue({ lobby: null, ticketId: null }); }); async function renderSidebar() { @@ -86,4 +93,12 @@ describe('Sidebar friend badge', () => { rerender(); expect(screen.getByText('3')).toBeInTheDocument(); }); + + it('links Active Lobby only when the authenticated user has one', async () => { + mockGetMyActive.mockResolvedValue({ lobby: { lobbyId: 'lobby-real' }, ticketId: null }); + await renderSidebar(); + const link = await screen.findByRole('link', { name: 'Active Lobby' }); + expect(link).toHaveAttribute('href', '/lobby/lobby-real'); + expect(link).not.toHaveAttribute('href', expect.stringContaining('SP-7F-29')); + }); }); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 830439b..8ceeb2e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,5 +1,5 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Icon } from '@/components/ui/Icons'; @@ -7,9 +7,9 @@ import { Avatar } from '@/components/ui/Avatar'; import { ROUTES } from '@/lib/routes'; import { useAuth } from '@/features/auth/AuthProvider'; import { useFriendSummary } from '@/features/friends/FriendSummaryContext'; +import { lobbyApi } from '@/features/lobby/lobbyApi'; -const NAV_SESSION = [ - { href: ROUTES.lobby('SP-7F-29'), label: 'Active Lobby', icon: 'controller' }, +const NAV_SESSION_BASE = [ { href: ROUTES.profile('me'), label: 'My Profile', icon: 'user' }, ]; const NAV_META = [ @@ -19,6 +19,18 @@ const NAV_META = [ export function Sidebar() { const pathname = usePathname(); const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/'); + const { user } = useAuth(); + const userId = user?.id; + const [activeLobbyId, setActiveLobbyId] = useState(null); + + useEffect(() => { + let cancelled = false; + if (!userId) return () => { cancelled = true; }; + lobbyApi.getMyActive() + .then((context) => { if (!cancelled) setActiveLobbyId(context.lobby?.lobbyId ?? null); }) + .catch(() => { if (!cancelled) setActiveLobbyId(null); }); + return () => { cancelled = true; }; + }, [userId]); const { summary, loading, error } = useFriendSummary(); const friendBadge = (!loading && !error && summary) ? summary.incomingRequestCount : 0; @@ -29,6 +41,10 @@ export function Sidebar() { { href: ROUTES.friends, label: 'Friends', icon: 'users', badgeCount: friendBadge }, { href: ROUTES.leaderboards, label: 'Leaderboards', icon: 'trophy' }, ]; + const NAV_SESSION = [ + ...(userId && activeLobbyId ? [{ href: ROUTES.lobby(activeLobbyId), label: 'Active Lobby', icon: 'controller' }] : []), + ...NAV_SESSION_BASE, + ]; return (