// @vitest-environment jsdom import {afterEach, describe, expect, it, vi} from 'vitest'; import {cleanup, fireEvent, screen} from '@testing-library/react'; import {toast} from 'sonner'; import {useTranslation} from 'react-i18next'; import AppHeader from './AppHeader.jsx'; import {fetchRoutes, json} from '../test/fetchRoutes.js'; import {renderWithProviders} from '../test/renderWithProviders.jsx'; vi.mock('react-i18next', () => import('../test/i18n-mock.js')); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}})); afterEach(() => { cleanup(); localStorage.clear(); useTranslation().i18n.language = 'de'; }); const loggedInAs = (me, routes = {}, options = {}) => { localStorage.setItem('tbp_session', '1'); fetchRoutes({'GET /api/users/me': json(200, me), 'GET /api/library/steam-profile': json(200, {}), 'GET /api/inbox': json(200, {results: [], unread: 0}), 'GET /api/library/questions/count': json(200, {count: 0}), 'GET /api/library/removed-games': json(200, {results: []}), ...routes}); renderWithProviders( {}}/>, options); }; const openMenu = () => fireEvent.keyDown(screen.getByRole('button', {name: 'NAV.MENU'}), {key: 'Enter'}); describe('AppHeader', () => { it('says so when Auth refuses the language switch', async () => { loggedInAs({role: 'USER', activeRole: 'USER', settings: {language: 'de'}}, {'PUT /api/users/settings': json(500, {})}); await screen.findByRole('button', {name: 'NAV.MENU'}); openMenu(); fireEvent.click(await screen.findByRole('menuitem', {name: /NAV.LANGUAGE/})); await vi.waitFor(() => expect(toast.error).toHaveBeenCalledWith('SETTINGS.ERROR_SAVE')); }); // jsdom applies no media queries, so the test hides the name the way the 520px breakpoint does. it('keeps a name on the home link when a narrow phone hides the app name', () => { fetchRoutes({}); renderWithProviders( {}}/>); screen.getByText('ToBePlayed').style.display = 'none'; expect(screen.getByRole('link', {name: 'ToBePlayed'})).toBeTruthy(); expect(screen.queryByTitle('NAV.COMMUNITY')).toBeNull(); }); it('links to the following page when logged in', () => { loggedInAs({role: 'USER', activeRole: 'USER'}); expect(screen.getByTitle('NAV.FOLLOWING').getAttribute('href')).toBe('/following'); }); it('links to the game search when logged in', () => { loggedInAs({role: 'USER', activeRole: 'USER'}); expect(screen.getByTitle('NAV.SEARCH').getAttribute('href')).toBe('/search'); }); it('links to the community stats when logged in', () => { loggedInAs({role: 'USER', activeRole: 'USER'}); const link = screen.getByTitle('NAV.COMMUNITY'); expect(link.getAttribute('href')).toBe('/'); expect(link.getAttribute('aria-current')).toBe('page'); }); it('shows the community link as an icon without a text label', () => { loggedInAs({role: 'USER', activeRole: 'USER'}); expect(screen.getByTitle('NAV.COMMUNITY').textContent).toBe(''); }); it('marks community active only on the landing page', () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {}, {entries: ['/dashboard']}); expect(screen.getByTitle('NAV.COMMUNITY').getAttribute('aria-current')).toBeNull(); expect(screen.getByTitle('NAV.DASHBOARD').getAttribute('aria-current')).toBe('page'); }); it('shows how many inbox notes are unread', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/inbox': json(200, {results: [], unread: 3})}); const link = await screen.findByRole('link', {name: 'NAV.INBOX_UNREAD:3'}); expect(link.getAttribute('href')).toBe('/inbox'); expect(link.textContent).toBe('3'); }); it('shows no count without unread notes', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}); const link = await screen.findByRole('link', {name: 'NAV.INBOX'}); expect(link.textContent).toBe(''); }); it('shows how many questions are open', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/questions/count': json(200, {count: 12})}); const link = await screen.findByRole('link', {name: 'NAV.QUESTIONS_OPEN:12'}); expect(link.getAttribute('href')).toBe('/questions'); expect(link.textContent).toBe('NAV.QUESTIONS9+'); }); it('hides the questions icon below sm while no question is open', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}); expect((await screen.findByTitle('NAV.QUESTIONS')).className).toContain('max-sm:hidden'); }); it('keeps the questions icon at every width while questions are open', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/questions/count': json(200, {count: 2})}); expect((await screen.findByRole('link', {name: 'NAV.QUESTIONS_OPEN:2'})).className).not.toContain('max-sm:hidden'); }); it('keeps the questions icon at every width while the user has removed games', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/removed-games': json(200, {results: [{appid: 1, name: 'Game'}]})}); await vi.waitFor(() => expect(screen.getByTitle('NAV.QUESTIONS').className).not.toContain('max-sm:hidden')); }); it('skipsTheRemovedGamesCallWhileQuestionsAreOpen', async () => { const removed = vi.fn(() => json(200, {results: []})); loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/questions/count': json(200, {count: 2}), 'GET /api/library/removed-games': removed}); await screen.findByRole('link', {name: 'NAV.QUESTIONS_OPEN:2'}); await new Promise((resolve) => setTimeout(resolve, 0)); expect(removed).not.toHaveBeenCalled(); }); it('keeps the questions icon below sm on the questions page, without a badge', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/questions/count': json(200, {count: 0})}, {entries: ['/questions']}); expect((await screen.findByTitle('NAV.QUESTIONS')).className).not.toContain('max-sm:hidden'); }); it('shows no question count on the questions page, whose title has it', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}, {'GET /api/library/questions/count': json(200, {count: 3})}, {entries: ['/questions']}); await vi.waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/library/questions/count'), expect.anything())); expect(screen.getByTitle('NAV.QUESTIONS').textContent).toBe('NAV.QUESTIONS'); expect(screen.queryByRole('link', {name: 'NAV.QUESTIONS_OPEN:3'})).toBeNull(); }); it('offers the admin page and "view as" to an admin', async () => { loggedInAs({role: 'PLATFORM_ADMIN', activeRole: 'PLATFORM_ADMIN'}, {'GET /api/admin/users/pending-count': json(200, {count: 0})}); await screen.findByRole('button', {name: 'NAV.MENU'}); await vi.waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/users/me'), expect.anything())); openMenu(); expect(await screen.findByText('NAV.ADMIN')).toBeTruthy(); expect(screen.getByText('NAV.ADMIN_NEWS')).toBeTruthy(); expect(screen.getByText('NAV.ADMIN_BROADCASTS')).toBeTruthy(); expect(screen.getByText('NAV.VIEW_AS.MENU')).toBeTruthy(); expect(screen.queryByText('NAV.VIEW_AS.BADGE')).toBeNull(); expect(screen.queryByLabelText('NAV.PENDING_REQUESTS:0')).toBeNull(); }); it('shows the admin how many registrations wait', async () => { loggedInAs({role: 'PLATFORM_ADMIN', activeRole: 'PLATFORM_ADMIN'}, {'GET /api/admin/users/pending-count': json(200, {count: 3})}); expect(await screen.findByLabelText('NAV.PENDING_REQUESTS:3')).toBeTruthy(); openMenu(); expect((await screen.findByRole('menuitem', {name: /^NAV\.ADMIN ?3$/})).textContent).toContain('3'); }); it('shows no count when Auth does not know the call yet', async () => { loggedInAs({role: 'PLATFORM_ADMIN', activeRole: 'PLATFORM_ADMIN'}, {'GET /api/admin/users/pending-count': json(404, {})}); await vi.waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('pending-count'), expect.anything())); expect(screen.queryByLabelText(/NAV.PENDING_REQUESTS/)).toBeNull(); }); it('shows the badge and hides the admin page while an admin views as a user', async () => { loggedInAs({role: 'PLATFORM_ADMIN', activeRole: 'USER'}); expect(await screen.findByText('NAV.VIEW_AS.BADGE')).toBeTruthy(); openMenu(); expect(await screen.findByText('NAV.VIEW_AS.MENU')).toBeTruthy(); expect(screen.queryByText('NAV.ADMIN')).toBeNull(); expect(screen.queryByText('NAV.ADMIN_BROADCASTS')).toBeNull(); }); it('offersTheCatalogLinkQueueAndAuditLogToAnAuditorOnly', async () => { loggedInAs({role: 'AUDITOR', activeRole: 'AUDITOR'}); await vi.waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/users/me'), expect.anything())); openMenu(); expect(await screen.findByText('NAV.GAME_CATALOG')).toBeTruthy(); expect(screen.getByText('NAV.LINK_QUEUE')).toBeTruthy(); expect(screen.getByText('NAV.AUDIT_LOG')).toBeTruthy(); expect(screen.queryByText('NAV.ADMIN')).toBeNull(); }); it('offers nothing of that to a user', async () => { loggedInAs({role: 'USER', activeRole: 'USER'}); await vi.waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/users/me'), expect.anything())); openMenu(); expect(await screen.findByText('NAV.SETTINGS')).toBeTruthy(); expect(screen.getByText('NAV.WHATS_NEW')).toBeTruthy(); expect(screen.queryByText('NAV.ADMIN')).toBeNull(); expect(screen.queryByText('NAV.LINK_QUEUE')).toBeNull(); expect(screen.queryByText('NAV.GAME_CATALOG')).toBeNull(); expect(screen.queryByText('NAV.ADMIN_NEWS')).toBeNull(); expect(screen.queryByText('NAV.ADMIN_BROADCASTS')).toBeNull(); expect(screen.queryByText('NAV.VIEW_AS.MENU')).toBeNull(); }); });