Public Access
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.
159 lines
6.9 KiB
React
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();
|
|
});
|
|
});
|