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

+46 -56
View File
@@ -1,25 +1,39 @@
// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
import {MemoryRouter, Route, Routes, useLocation} from 'react-router-dom';
import RequireSession from '../components/RequireSession.jsx';
import {cleanup, fireEvent, screen, waitFor} from '@testing-library/react';
import Dashboard from './Dashboard.jsx';
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
import {CurrentUserProvider} from '../context/CurrentUserContext.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('../hooks/useLibrarySync.js', () => ({default: () => ({status: 'OK', lastSyncedAt: null, isRunning: false})}));
vi.mock('../hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), busy: false})}));
const current = {location: null};
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
const games = Array.from({length: 12}, (_, i) => ({appid: 100 + i, name: `Game ${i}`, playtimeMinutes: 1000 - i}));
const empty = {content: [], total: 0, byStatus: {}};
const mostPlayed = (url) => url.includes('sort=playtimeMinutes') && !url.includes('status=');
const quietRoutes = {
'GET /api/library/summary': json(200, empty),
'GET /api/hints': json(200, {results: []}),
};
function Spy() {
current.location = useLocation();
return null;
}
// Every request of the page with a Steam ID; the test of the 401 answers all but the user with 401.
const dashboardRoutes = (settings) => ({
...quietRoutes,
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000', settings}),
'GET /api/library/games': (url) => json(200, mostPlayed(url) ? {content: games, total: 12} : empty),
'GET /api/library/stats': json(200, {trend: null, habits: {moonTrips: 1, longestDay: null, underOneHourGames: 0, underOneHourShare: null}}),
'GET /api/library/steam-profile': json(200, {personaName: 'Lobo', avatarUrl: avatar}),
'GET /api/library/activity': json(200, {totalMinutes: 90, days: [{day: '2026-09-23', minutes: 0}, {day: '2026-09-24', minutes: 90}], recentGames: [
{appid: 1086940, name: "Baldur's Gate 3", playtimeMinutes: 900, playtime2Weeks: 90, status: 'PLAYING'}]}),
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}),
'GET /api/library/achievements/summary': json(200, {unlocked: 24, startedGames: 4, perfectGames: 2,
averageCompletion: 71.3, pendingGames: 0, hiddenGames: 0, closest: []}),
'PUT /api/users/settings/dashboard-tiles': json(204, {}),
});
const ok = (body) => ({ok: true, status: 200, json: async () => body});
const unauthorized = () => ({ok: false, status: 401, json: async () => ({})});
const renderDashboard = () => renderWithProviders(<Dashboard/>, {entries: ['/dashboard']});
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
@@ -32,45 +46,24 @@ afterEach(() => {
describe('Dashboard', () => {
it('opens the login page when the core API answers 401', async () => {
globalThis.fetch = vi.fn(async (url) =>
url.includes('/api/users/me') ? ok({email: 'a@b.de', steamId: '76561198000000000'}) : unauthorized());
render(
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider><CurrentUserProvider>
<Routes>
<Route element={<RequireSession/>}><Route path="/dashboard" element={<><Dashboard/><Spy/></>}/></Route>
<Route path="/login" element={<Spy/>}/>
</Routes>
</CurrentUserProvider></PreferencesProvider></MemoryRouter>,
);
fetchRoutes({
...Object.fromEntries(Object.keys(dashboardRoutes()).map((key) => [key, json(401, {})])),
'GET /api/users/me': json(200, {email: 'a@b.de', steamId: '76561198000000000'}),
'POST /api/auth/refresh': json(401, {}),
});
const {router} = renderWithProviders(<Dashboard/>,
{path: '/dashboard', entries: ['/dashboard'], guarded: true, routes: {'/login': <p>login</p>}});
await waitFor(() => expect(fetch.mock.calls.length).toBeGreaterThanOrEqual(5));
await waitFor(() => expect(current.location.pathname).toBe('/login'));
expect(current.location.state).toEqual({sessionExpired: true, from: '/dashboard'});
await waitFor(() => expect(router.state.location.pathname).toBe('/login'));
expect(router.state.location.state).toEqual({sessionExpired: true, from: '/dashboard'});
expect(screen.queryByText('LIBRARY.EMPTY')).toBeNull();
});
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
const games = Array.from({length: 12}, (_, i) => ({appid: 100 + i, name: `Game ${i}`, playtimeMinutes: 1000 - i}));
const mockDashboard = (settings) => {
globalThis.fetch = vi.fn(async (url, init) => {
if (init?.method === 'PUT') return {ok: true, status: 204, json: async () => ({})};
if (url.includes('/api/users/me')) return ok({email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000', settings});
if (url.includes('sort=playtimeMinutes') && !url.includes('status=')) return ok({content: games, total: 12});
if (url.includes('/api/library/stats')) return ok({trend: null, habits: {moonTrips: 1, longestDay: null, underOneHourGames: 0, underOneHourShare: null}});
if (url.includes('/api/library/steam-profile')) return ok({personaName: 'Lobo', avatarUrl: avatar});
if (url.includes('/api/library/activity')) return ok({totalMinutes: 90, days: [{day: '2026-09-23', minutes: 0}, {day: '2026-09-24', minutes: 90}], recentGames: [
{appid: 1086940, name: "Baldur's Gate 3", playtimeMinutes: 900, playtime2Weeks: 90, status: 'PLAYING'}]});
if (url.includes('/api/library/games-count')) return ok({count: 1, hasSteamId: true});
if (url.includes('/api/library/achievements/summary')) return ok({unlocked: 24, startedGames: 4, perfectGames: 2,
averageCompletion: 71.3, pendingGames: 0, hiddenGames: 0, closest: []});
return ok({content: [], total: 0, byStatus: {}});
});
return render(
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider><CurrentUserProvider>
<Dashboard/>
</CurrentUserProvider></PreferencesProvider></MemoryRouter>,
).container;
fetchRoutes(dashboardRoutes(settings));
return renderDashboard().container;
};
it('shows the Steam avatar and the most played game of the last two weeks as banner', async () => {
@@ -154,19 +147,16 @@ describe('Dashboard', () => {
});
it('shows the page when a card answers 200 without a JSON body', async () => {
// No Steam ID, so no activity and achievements are asked.
globalThis.fetch = vi.fn(async (url) => {
if (url.includes('/api/users/me')) return ok({email: 'a@b.de', displayName: 'Lobo'});
if (url.includes('/api/library/games-count')) return ok({count: 1, hasSteamId: true});
if (url.includes('sort=playtimeMinutes') && !url.includes('status=')) {
return {ok: true, status: 200, json: async () => { throw new SyntaxError('Unexpected token <'); }};
}
return ok({content: [], total: 0, byStatus: {}});
fetchRoutes({
...quietRoutes,
'GET /api/library/stats': json(200, empty),
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo'}),
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}),
'GET /api/library/games': (url) => (mostPlayed(url)
? {ok: true, status: 200, json: async () => { throw new SyntaxError('Unexpected token <'); }}
: json(200, empty)),
});
render(
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider><CurrentUserProvider>
<Dashboard/>
</CurrentUserProvider></PreferencesProvider></MemoryRouter>,
);
renderDashboard();
expect(await screen.findByText(/DASHBOARD.HELLO/)).toBeTruthy();
});