// @vitest-environment jsdom import {afterEach, describe, expect, it, vi} from 'vitest'; import {act, cleanup, fireEvent, render, screen} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import StatGamesDialog from './StatGamesDialog.jsx'; import {StatCount, StatDrillProvider} from './StatDrill.jsx'; import {fetchRoutes, json} from '../../test/fetchRoutes.js'; vi.mock('react-i18next', () => import('../../test/i18n-mock.js')); const GAMES = '/api/library/stats/games'; const rows = (from, count, extra = {}) => Array.from({length: count}, (_, i) => ( {appid: from + i, name: `Game ${from + i}`, status: 'BACKLOG', playtimeMinutes: 60, ...extra})); const page = (content, index, totalPages, totalElements) => json(200, {content, page: index, size: 40, totalElements, totalPages}); const renderDialog = (request, onClose = () => {}) => render( ); describe('StatGamesDialog', () => { afterEach(cleanup); it('loadsPageZeroAndAppendsTheNextOne', async () => { fetchRoutes({[`GET ${GAMES}`]: (url) => url.includes('page=1') ? page(rows(41, 5), 1, 2, 45) : page(rows(1, 40), 0, 2, 45)}); renderDialog(); expect(await screen.findByText('Game 40')).toBeTruthy(); expect(screen.getAllByRole('listitem')).toHaveLength(40); expect(fetch.mock.calls[0][0]).toContain('key=GENRE&arg=Shooter&page=0'); fireEvent.click(screen.getByRole('button', {name: 'STATS.DRILL.LOAD_MORE'})); expect(await screen.findByText('Game 45')).toBeTruthy(); expect(screen.getAllByRole('listitem')).toHaveLength(45); expect(screen.queryByRole('button', {name: 'STATS.DRILL.LOAD_MORE'})).toBeNull(); }); it('closesOnEscape', async () => { fetchRoutes({[`GET ${GAMES}`]: page(rows(1, 1), 0, 1, 1)}); const onClose = vi.fn(); renderDialog({}, onClose); await screen.findByText('Game 1'); fireEvent.keyDown(screen.getByRole('dialog'), {key: 'Escape'}); expect(onClose).toHaveBeenCalled(); }); it('offersARetryAfterAnError', async () => { let fail = true; fetchRoutes({[`GET ${GAMES}`]: () => (fail ? json(500, {}) : page(rows(1, 1), 0, 1, 1))}); renderDialog(); expect(await screen.findByText('STATS.DRILL.ERROR')).toBeTruthy(); fail = false; fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.RETRY'})); expect(await screen.findByText('Game 1')).toBeTruthy(); }); it('saysSoWhenTheOwnerDoesNotShareTheNumber', async () => { fetchRoutes({'GET /api/public/profiles/library/stats/games': json(403, {code: 'NOT_SHARED'})}); renderDialog({source: 'profile', ticket: 't'}); expect(await screen.findByText('STATS.DRILL.NOT_SHARED')).toBeTruthy(); expect(screen.queryByRole('button', {name: 'DASHBOARD.RETRY'})).toBeNull(); }); it('asksForAReloadWhenTheTicketExpired', async () => { const mock = fetchRoutes({'GET /api/public/profiles/library/stats/games': json(401, {code: 'INVALID_TICKET'})}); renderDialog({source: 'profile', ticket: 'old'}); expect(await screen.findByText('STATS.DRILL.EXPIRED')).toBeTruthy(); expect(screen.getByRole('button', {name: 'STATS.DRILL.RELOAD'})).toBeTruthy(); expect(mock.mock.calls[0][1].headers['X-Profile-Ticket']).toBe('old'); }); it('showsTheStatusMoveOfATrendRow', async () => { fetchRoutes({[`GET ${GAMES}`]: page( [{appid: 5, name: 'Moved', fromStatus: 'BACKLOG', toStatus: 'PLAYING', at: '2026-09-20T10:00:00Z'}], 0, 1, 1)}); renderDialog({params: {key: 'TREND_MOVES'}}); await screen.findByText('Moved'); expect(screen.getByText(/BACKLOG\.STATUS\.BACKLOG → BACKLOG\.STATUS\.PLAYING/)).toBeTruthy(); }); it('showsTheLibraryCountOfACommunityRow', async () => { fetchRoutes({'GET /api/stats/public/games': page([{appid: 7, name: 'Shared', libraries: 3, minutes: 120}], 0, 1, 1)}); renderDialog({source: 'community', params: {key: 'OWNED'}}); await screen.findByText('Shared'); expect(screen.getByText('STATS.DRILL.IN_LIBRARIES:3 · 2.0h')).toBeTruthy(); }); it('focusReturnsToTheClickedNumber', async () => { fetchRoutes({[`GET ${GAMES}`]: page(rows(1, 1), 0, 1, 1)}); render( 1 ); const number = screen.getByRole('button', {name: 'Played: 1'}); number.focus(); fireEvent.click(number); await screen.findByText('Game 1'); fireEvent.keyDown(screen.getByRole('dialog'), {key: 'Escape'}); await act(async () => {}); expect(document.activeElement).toBe(number); }); it('aLateAnswerOfAClosedListDoesNotFillTheOpenOne', async () => { let answerOld; fetchRoutes({[`GET ${GAMES}`]: (url) => (url.includes('arg=Old') ? new Promise((resolve) => { answerOld = resolve; }) : page([{appid: 2, name: 'New game', playtimeMinutes: 1}], 0, 1, 1))}); const request = (arg) => ({title: arg, source: 'own', params: {key: 'GENRE', arg}}); const view = render( {}}/>); view.rerender( {}}/>); await screen.findByText('New game'); await act(async () => answerOld(page([{appid: 1, name: 'Old game', playtimeMinutes: 1}], 0, 1, 1))); expect(screen.queryByText('Old game')).toBeNull(); expect(screen.getByText('New game')).toBeTruthy(); }); it('aProfileRowIsPlainForAVisitor', async () => { fetchRoutes({'GET /api/public/profiles/library/stats/games': page(rows(1, 1), 0, 1, 1)}); renderDialog({source: 'profile', ticket: 't'}); await screen.findByText('Game 1'); expect(screen.queryByRole('link')).toBeNull(); }); it('aProfileRowOpensTheDetailPageForTheOwner', async () => { fetchRoutes({'GET /api/public/profiles/library/stats/games': page(rows(1, 1), 0, 1, 1)}); renderDialog({source: 'profile', ticket: 't', linked: true}); const link = await screen.findByRole('link', {name: /Game 1/}); expect(link.getAttribute('href')).toBe('/library/1'); }); it('aCommunityRowIsPlain', async () => { fetchRoutes({'GET /api/stats/public/games': page(rows(1, 1, {libraries: 2, minutes: 60}), 0, 1, 1)}); renderDialog({source: 'community'}); await screen.findByText('Game 1'); expect(screen.queryByRole('link')).toBeNull(); }); });