test: data router, one render helper, fetchRoutes in every test

This commit is contained in:
lobo committed 2026-09-29 16:15:50 +02:00
1 parent d0ea9e900d
commit 2a22789435
37 files changed
+454 -425

No files matched your search

+22 -20
View File
@@ -1,9 +1,10 @@
// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {act, cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
import {MemoryRouter, Route, Routes, useLocation, useNavigate} from 'react-router-dom';
import {createMemoryRouter, RouterProvider, useLocation, useNavigate} from 'react-router-dom';
import GameLibrary from './GameLibrary.jsx';
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('../hooks/useLibrarySync.js', () => ({default: () => ({status: 'OK', lastSyncedAt: null, isRunning: false})}));
@@ -12,9 +13,14 @@ vi.mock('../hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), b
const DEBOUNCE_WAIT_MS = 700;
const page = {content: [{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG'}], page: 0, size: 20, totalElements: 1, totalPages: 1};
const answer = (games = page.content, hasSteamId = true) => fetchRoutes({
'GET /api/library/games': json(200, {...page, content: games, totalElements: games.length, totalPages: games.length ? 1 : 0}),
'GET /api/library/games-count': json(200, {count: games.length, hasSteamId}),
});
const wait = (ms) => act(() => new Promise((resolve) => setTimeout(resolve, ms)));
// MemoryRouter, not createMemoryRouter: it pushes like the BrowserRouter in main.jsx, also for an unchanged URL.
// A data router like main.jsx: a navigation to the same URL replaces the entry.
function renderLibrary(initialEntry = '/library') {
const lib = {navigations: 0, location: null, navigate: null};
function Spy() {
@@ -26,11 +32,11 @@ function renderLibrary(initialEntry = '/library') {
}
return null;
}
render(
<MemoryRouter initialEntries={[initialEntry]}><PreferencesProvider>
<Routes><Route path="/library" element={<><GameLibrary/><Spy/></>}/></Routes>
</PreferencesProvider></MemoryRouter>,
);
const router = createMemoryRouter([{
path: '/library',
element: <PreferencesProvider><GameLibrary/><Spy/></PreferencesProvider>,
}], {initialEntries: [initialEntry]});
render(<RouterProvider router={router}/>);
return {
navigations: () => lib.navigations,
params: () => new URLSearchParams(lib.location.search),
@@ -42,7 +48,7 @@ const typeInto = (value) => fireEvent.change(screen.getByRole('searchbox'), {tar
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => page}));
answer();
});
afterEach(() => {
@@ -52,10 +58,10 @@ afterEach(() => {
describe('GameLibrary search', () => {
it('shows the achievement progress of a game when it is known', async () => {
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => ({...page, content: [
answer([
{...page.content[0], achievementsUnlocked: 3, achievementsTotal: 4},
{appid: 620, name: 'Portal 2', playtimeMinutes: 0, status: 'BACKLOG', achievementsUnlocked: null, achievementsTotal: 5},
]})}));
]);
renderLibrary();
expect(await screen.findByText('3/4')).toBeTruthy();
@@ -100,7 +106,7 @@ describe('GameLibrary search', () => {
typeInto('half');
await waitFor(() => expect(lib.params().get('q')).toBe('half'));
act(() => lib.back());
await act(async () => lib.back());
await waitFor(() => expect(screen.getByRole('searchbox').value).toBe(''));
});
@@ -161,7 +167,7 @@ describe('GameLibrary short games', () => {
});
it('says so when no game is hidden', async () => {
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => ({...page, content: [], totalElements: 0, totalPages: 0})}));
answer([]);
renderLibrary('/library?hidden=true');
expect(await screen.findByText('LIBRARY.NO_HIDDEN')).toBeTruthy();
@@ -169,15 +175,11 @@ describe('GameLibrary short games', () => {
});
describe('GameLibrary with games added by hand', () => {
const answer = (games, hasSteamId) => vi.fn(async (url) => ({
ok: true, status: 200,
json: async () => (String(url).includes('/games-count') ? {count: games.length, hasSteamId} : {...page, content: games}),
}));
const zelda = {appid: -1025, name: 'Zelda', playtimeMinutes: 90, status: 'BACKLOG', source: 'MANUAL',
platform: 'Nintendo Switch', imageUrl: 'https://images.igdb.com/z.jpg'};
it('shows the platform and the IGDB picture of a manual game', async () => {
globalThis.fetch = answer([zelda], true);
answer([zelda], true);
renderLibrary();
expect(await screen.findByText('Nintendo Switch')).toBeTruthy();
@@ -185,7 +187,7 @@ describe('GameLibrary with games added by hand', () => {
});
it('shows Steam as the platform of a Steam game, and links the import', async () => {
globalThis.fetch = answer([{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG', source: 'STEAM',
answer([{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG', source: 'STEAM',
platform: null}], true);
renderLibrary();
@@ -194,7 +196,7 @@ describe('GameLibrary with games added by hand', () => {
});
it('hides the Steam sync for a user without a Steam ID but still offers adding a game', async () => {
globalThis.fetch = answer([zelda], false);
answer([zelda], false);
renderLibrary();
await screen.findByText('Zelda');
@@ -203,7 +205,7 @@ describe('GameLibrary with games added by hand', () => {
});
it('opens the add panel', async () => {
globalThis.fetch = answer([zelda], true);
answer([zelda], true);
renderLibrary();
fireEvent.click(await screen.findByRole('button', {name: 'MANUAL_GAME.ADD_BUTTON'}));