From 533f0cad4a0f512535db27907e30852df3c4a964 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 11:42:20 +0100 Subject: [PATCH] fix(tests): mock useAuth in Layout tests for auth-aware navigation Layout now renders different nav links based on auth state. Tests were using a real AuthProvider which resolved to no user, causing them to look for "My Runs" and "Admin" links that only appear when logged in. Mock useAuth to test both logged-out and logged-in-as-admin states. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/Layout.test.tsx | 96 ++++++++++++++++++------- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/Layout.test.tsx b/frontend/src/components/Layout.test.tsx index cd14506..293c668 100644 --- a/frontend/src/components/Layout.test.tsx +++ b/frontend/src/components/Layout.test.tsx @@ -2,62 +2,108 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import { Layout } from './Layout' -import { AuthProvider } from '../contexts/AuthContext' vi.mock('../hooks/useTheme', () => ({ useTheme: () => ({ theme: 'dark' as const, toggle: vi.fn() }), })) +const mockUseAuth = vi.fn() +vi.mock('../contexts/AuthContext', () => ({ + useAuth: () => mockUseAuth(), +})) + +const loggedOutAuth = { + user: null, + session: null, + loading: false, + isAdmin: false, + signInWithEmail: vi.fn(), + signUpWithEmail: vi.fn(), + signInWithGoogle: vi.fn(), + signInWithDiscord: vi.fn(), + signOut: vi.fn(), +} + +const adminAuth = { + ...loggedOutAuth, + user: { email: 'admin@example.com' }, + session: {}, + isAdmin: true, +} + function renderLayout(initialPath = '/') { return render( - - - + ) } describe('Layout', () => { - it('renders all desktop navigation links', () => { - renderLayout() - expect(screen.getAllByRole('link', { name: /new run/i })[0]).toBeInTheDocument() - expect(screen.getAllByRole('link', { name: /my runs/i })[0]).toBeInTheDocument() - expect(screen.getAllByRole('link', { name: /genlockes/i })[0]).toBeInTheDocument() - expect(screen.getAllByRole('link', { name: /stats/i })[0]).toBeInTheDocument() - expect(screen.getAllByRole('link', { name: /admin/i })[0]).toBeInTheDocument() + describe('when logged out', () => { + beforeEach(() => mockUseAuth.mockReturnValue(loggedOutAuth)) + + it('renders logged-out navigation links', () => { + renderLayout() + expect(screen.getAllByRole('link', { name: /^home$/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /^runs$/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /genlockes/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /stats/i })[0]).toBeInTheDocument() + }) + + it('does not show authenticated links', () => { + renderLayout() + expect(screen.queryByRole('link', { name: /new run/i })).not.toBeInTheDocument() + expect(screen.queryByRole('link', { name: /my runs/i })).not.toBeInTheDocument() + expect(screen.queryByRole('link', { name: /admin/i })).not.toBeInTheDocument() + }) + + it('shows sign-in link', () => { + renderLayout() + expect(screen.getByRole('link', { name: /sign in/i })).toBeInTheDocument() + }) + }) + + describe('when logged in as admin', () => { + beforeEach(() => mockUseAuth.mockReturnValue(adminAuth)) + + it('renders authenticated navigation links', () => { + renderLayout() + expect(screen.getAllByRole('link', { name: /new run/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /my runs/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /genlockes/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /stats/i })[0]).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /admin/i })[0]).toBeInTheDocument() + }) + + it('shows the mobile dropdown when the hamburger is clicked', async () => { + renderLayout() + const hamburger = screen.getByRole('button', { name: /toggle menu/i }) + await userEvent.click(hamburger) + expect(screen.getAllByRole('link', { name: /my runs/i }).length).toBeGreaterThan(1) + }) }) it('renders the brand logo link', () => { + mockUseAuth.mockReturnValue(loggedOutAuth) renderLayout() expect(screen.getByRole('link', { name: /ant/i })).toBeInTheDocument() }) it('renders the theme toggle button', () => { + mockUseAuth.mockReturnValue(loggedOutAuth) renderLayout() expect(screen.getAllByRole('button', { name: /switch to light mode/i })[0]).toBeInTheDocument() }) it('initially hides the mobile dropdown menu', () => { + mockUseAuth.mockReturnValue(loggedOutAuth) renderLayout() - // Mobile menu items exist in DOM but menu is hidden; the mobile dropdown - // only appears inside the sm:hidden block after state toggle. - // The hamburger button should be present. expect(screen.getByRole('button', { name: /toggle menu/i })).toBeInTheDocument() }) - it('shows the mobile dropdown when the hamburger is clicked', async () => { - renderLayout() - const hamburger = screen.getByRole('button', { name: /toggle menu/i }) - await userEvent.click(hamburger) - // After click, the menu open state adds a dropdown with nav links - // We can verify the menu is open by checking a class change or that - // the nav links appear in the mobile dropdown section. - // The mobile dropdown renders navLinks in a div inside sm:hidden - expect(screen.getAllByRole('link', { name: /my runs/i }).length).toBeGreaterThan(1) - }) - it('renders the footer with PokeDB attribution', () => { + mockUseAuth.mockReturnValue(loggedOutAuth) renderLayout() expect(screen.getByRole('link', { name: /pokedb/i })).toBeInTheDocument() })