Files
ToBePlayed-Frontend/src/components/stats/StatGamesDialog.test.jsx
T
lobo 8b94a25844 fix(stats): open game detail pages from stat lists, perfect games clickable
Add perfect games click-through on the dashboard. Remove Steam store
links from game lists. Stat numbers with count 0 are not clickable.
Tests cover the perfect-games click, zero guards, and owner versus
visitor behavior.
2026-10-04 10:07:27 +02:00

159 lines
6.9 KiB
React

// @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(
<MemoryRouter><StatGamesDialog request={{title: 'Shooter', source: 'own', params: {key: 'GENRE', arg: 'Shooter'}, ...request}}
onClose={onClose}/></MemoryRouter>);
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(<MemoryRouter><StatDrillProvider source="own">
<StatCount params={{key: 'PLAYED'}} title="Played" label="Played: 1">1</StatCount>
</StatDrillProvider></MemoryRouter>);
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(<MemoryRouter><StatGamesDialog request={request('Old')} onClose={() => {}}/></MemoryRouter>);
view.rerender(<MemoryRouter><StatGamesDialog request={request('New')} onClose={() => {}}/></MemoryRouter>);
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();
});
});