Public Access
test: data router, one render helper, fetchRoutes in every test
This commit is contained in:
1 parent
d0ea9e900d
commit
2a22789435
37 files changed
+454
-425
No files matched your search
@@ -1,11 +1,10 @@
|
||||
// @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} from 'react-router-dom';
|
||||
import {cleanup, fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import {toast} from 'sonner';
|
||||
import AddGamePanel from './AddGamePanel.jsx';
|
||||
import {CurrentUserProvider} from '../context/CurrentUserContext.jsx';
|
||||
import {json} from '../test/fetchRoutes.js';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import {renderWithProviders} from '../test/renderWithProviders.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -17,20 +16,16 @@ const zelda = {
|
||||
const portal = {...zelda, appid: 400, igdbId: 71, name: 'Portal', inLibrary: true};
|
||||
|
||||
|
||||
function renderPanel(post = json(201, {appid: -1025}), favoritePlatform = 'Nintendo Switch') {
|
||||
globalThis.fetch = vi.fn(async (url, init) => {
|
||||
if (url.includes('/api/users/me')) return json(200, {role: 'USER', settings: {favoritePlatform}});
|
||||
return init?.method === 'POST' ? post : json(200, {available: true, results: [zelda, portal]});
|
||||
function renderPanel(post = json(201, {appid: -1025}), favoritePlatform = 'Nintendo Switch',
|
||||
found = json(200, {available: true, results: [zelda, portal]})) {
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {role: 'USER', settings: {favoritePlatform}}),
|
||||
'GET /api/library/game-search': found,
|
||||
'POST /api/library/games': post,
|
||||
'POST /api/library/imports': post,
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/library']}><CurrentUserProvider>
|
||||
<Routes>
|
||||
<Route path="/library" element={<AddGamePanel onClose={() => {}}/>}/>
|
||||
<Route path="/library/:appid" element={<p>detail page</p>}/>
|
||||
<Route path="/import" element={<p>import page</p>}/>
|
||||
</Routes>
|
||||
</CurrentUserProvider></MemoryRouter>,
|
||||
);
|
||||
renderWithProviders(<AddGamePanel onClose={() => {}}/>, {path: '/library', entries: ['/library'],
|
||||
routes: {'/library/:appid': <p>detail page</p>, '/import': <p>import page</p>}});
|
||||
}
|
||||
|
||||
const searchCalls = () => fetch.mock.calls.filter(([url]) => url.includes('game-search'));
|
||||
@@ -119,9 +114,7 @@ describe('AddGamePanel', () => {
|
||||
});
|
||||
|
||||
it('shows that adding is off when Core has no IGDB app', async () => {
|
||||
renderPanel();
|
||||
fetch.mockImplementation(async (url) => (url.includes('/api/users/me')
|
||||
? json(200, {settings: {}}) : json(200, {available: false, results: []})));
|
||||
renderPanel(undefined, undefined, json(200, {available: false, results: []}));
|
||||
search('zelda');
|
||||
|
||||
expect(await screen.findByText('MANUAL_GAME.UNAVAILABLE')).toBeTruthy();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen} from '@testing-library/react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {cleanup, fireEvent, screen} from '@testing-library/react';
|
||||
import {toast} from 'sonner';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import AppHeader from './AppHeader.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('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -17,26 +16,18 @@ afterEach(() => {
|
||||
useTranslation().i18n.language = 'de';
|
||||
});
|
||||
|
||||
const loggedInAs = (me) => {
|
||||
const loggedInAs = (me, routes = {}) => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async (url) => ({
|
||||
ok: true, status: 200, json: async () => (url.includes('/api/users/me') ? me : {}),
|
||||
}));
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<PreferencesProvider><CurrentUserProvider><AppHeader isAuthenticated onLogout={() => {}}/></CurrentUserProvider></PreferencesProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
fetchRoutes({'GET /api/users/me': json(200, me), 'GET /api/library/steam-profile': json(200, {}), ...routes});
|
||||
renderWithProviders(<AppHeader isAuthenticated onLogout={() => {}}/>);
|
||||
};
|
||||
|
||||
const openMenu = () => fireEvent.keyDown(screen.getByRole('button', {name: 'NAV.MENU'}), {key: 'Enter'});
|
||||
|
||||
describe('AppHeader', () => {
|
||||
it('says so when Auth refuses the language switch', async () => {
|
||||
loggedInAs({role: 'USER', activeRole: 'USER', settings: {language: 'de'}});
|
||||
fetch.mockImplementation(async (url, init) => (init?.method === 'PUT'
|
||||
? {ok: false, status: 500, json: async () => ({})}
|
||||
: {ok: true, status: 200, json: async () => ({role: 'USER', activeRole: 'USER', settings: {language: 'de'}})}));
|
||||
loggedInAs({role: 'USER', activeRole: 'USER', settings: {language: 'de'}},
|
||||
{'PUT /api/users/settings': json(500, {})});
|
||||
|
||||
await screen.findByRole('button', {name: 'NAV.MENU'});
|
||||
openMenu();
|
||||
@@ -47,11 +38,8 @@ describe('AppHeader', () => {
|
||||
|
||||
// jsdom applies no media queries, so the test hides the name the way the 380px breakpoint does.
|
||||
it('keeps a name on the home link when a narrow phone hides the app name', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<PreferencesProvider><CurrentUserProvider><AppHeader isAuthenticated={false} onLogout={() => {}}/></CurrentUserProvider></PreferencesProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
fetchRoutes({});
|
||||
renderWithProviders(<AppHeader isAuthenticated={false} onLogout={() => {}}/>);
|
||||
screen.getByText('ToBePlayed').style.display = 'none';
|
||||
|
||||
expect(screen.getByRole('link', {name: 'ToBePlayed'})).toBeTruthy();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import BacklogCard from './BacklogCard.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
@@ -13,7 +14,7 @@ const saveButton = () => screen.getByRole('button', {name: 'BACKLOG.SAVE'});
|
||||
describe('BacklogCard', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async (url, init) => ({ok: true, status: 200, json: async () => JSON.parse(init.body)}));
|
||||
fetchRoutes({'PUT /api/library/games/220/backlog': (url, init) => json(200, JSON.parse(init.body))});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import CompletedHint from './CompletedHint.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
@@ -13,9 +14,8 @@ const renderHint = (props) => render(<MemoryRouter><CompletedHint game={game} on
|
||||
describe('CompletedHint', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async (url, init) => ({
|
||||
ok: true, status: 200, json: async () => ({...JSON.parse(init.body), rating: 5, notes: 'keep', priority: null}),
|
||||
}));
|
||||
fetchRoutes({'PUT /api/library/games/220/status': (url, init) =>
|
||||
json(200, {...JSON.parse(init.body), rating: 5, notes: 'keep', priority: null})});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {act, cleanup, fireEvent, render, screen} from '@testing-library/react';
|
||||
import {toast} from 'sonner';
|
||||
import DashboardHint, {AUTO_CLOSE_MS} from './DashboardHint.jsx';
|
||||
import {hintText} from '../utils/hints.js';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {error: vi.fn()}}));
|
||||
@@ -11,11 +12,13 @@ vi.mock('sonner', () => ({toast: {error: vi.fn()}}));
|
||||
const DISMISS = {id: 2, textEn: 'Server update tonight', textDe: 'Heute Nacht Update', kind: 'DISMISS'};
|
||||
const ACCEPT = {id: 1, textEn: 'Link your Steam account', textDe: null, kind: 'ACCEPT'};
|
||||
|
||||
const answer = (status, body) => ({ok: status < 300, status, headers: new Headers(), json: async () => body});
|
||||
const hintRoutes = (list, done = json(204)) => fetchRoutes({
|
||||
'GET /api/hints': list, 'PUT /api/hints/1/done': done, 'PUT /api/hints/2/done': done,
|
||||
});
|
||||
const doneCalls = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT').map(([url]) => url);
|
||||
|
||||
const showHints = async (...hints) => {
|
||||
globalThis.fetch = vi.fn(async (url, init) => (init?.method ? answer(204) : answer(200, {results: hints})));
|
||||
hintRoutes(json(200, {results: hints}));
|
||||
render(<DashboardHint/>);
|
||||
await act(async () => undefined);
|
||||
};
|
||||
@@ -88,7 +91,7 @@ describe('DashboardHint', () => {
|
||||
});
|
||||
|
||||
it('shows an error when the answer is not stored', async () => {
|
||||
globalThis.fetch = vi.fn(async (url, init) => (init?.method ? answer(500) : answer(200, {results: [ACCEPT]})));
|
||||
hintRoutes(json(200, {results: [ACCEPT]}), json(500));
|
||||
render(<DashboardHint/>);
|
||||
await act(async () => undefined);
|
||||
|
||||
@@ -109,7 +112,7 @@ describe('DashboardHint', () => {
|
||||
});
|
||||
|
||||
it('shows nothing when the hints do not load', async () => {
|
||||
globalThis.fetch = vi.fn(async () => answer(500));
|
||||
hintRoutes(json(500));
|
||||
render(<DashboardHint/>);
|
||||
await act(async () => undefined);
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import RequireSession from './RequireSession.jsx';
|
||||
import {toast} from 'sonner';
|
||||
import DeleteAccountCard from './DeleteAccountCard.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
|
||||
const EMAIL = 'player@example.com';
|
||||
|
||||
const answer = (status, headers = {}) => ({ok: status < 300, status, headers: new Headers(headers), json: async () => ({})});
|
||||
const deleteAnswers = (answer, routes = {}) => fetchRoutes({'DELETE /api/users/me': answer, ...routes});
|
||||
|
||||
const renderCard = () => render(
|
||||
<MemoryRouter initialEntries={['/settings']}>
|
||||
@@ -33,7 +34,7 @@ const confirmButton = () => screen.getByRole('button', {name: 'SETTINGS.DELETE_A
|
||||
describe('DeleteAccountCard', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async () => answer(204));
|
||||
deleteAnswers(json(204, {}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -68,7 +69,7 @@ describe('DeleteAccountCard', () => {
|
||||
});
|
||||
|
||||
it('keeps the dialog open on a wrong password', async () => {
|
||||
fetch.mockResolvedValue(answer(403));
|
||||
deleteAnswers(json(403, {}));
|
||||
renderCard();
|
||||
openAndFill(EMAIL, 'wrong');
|
||||
|
||||
@@ -80,7 +81,7 @@ describe('DeleteAccountCard', () => {
|
||||
});
|
||||
|
||||
it('shows the lock message after too many wrong passwords', async () => {
|
||||
fetch.mockResolvedValue(answer(429, {'Retry-After': '900'}));
|
||||
deleteAnswers({...json(429, {}), headers: new Headers({'Retry-After': '900'})});
|
||||
renderCard();
|
||||
openAndFill(EMAIL, 'wrong');
|
||||
|
||||
@@ -90,7 +91,7 @@ describe('DeleteAccountCard', () => {
|
||||
});
|
||||
|
||||
it('opens the login page when the session is gone', async () => {
|
||||
fetch.mockResolvedValue(answer(401));
|
||||
deleteAnswers(json(401, {}), {'POST /api/auth/refresh': json(401, {})});
|
||||
renderCard();
|
||||
openAndFill(EMAIL, 'secret');
|
||||
|
||||
@@ -100,7 +101,9 @@ describe('DeleteAccountCard', () => {
|
||||
});
|
||||
|
||||
it('shows the network error when Auth does not answer', async () => {
|
||||
fetch.mockRejectedValue(new TypeError('Failed to fetch'));
|
||||
deleteAnswers(() => {
|
||||
throw new TypeError('Failed to fetch');
|
||||
});
|
||||
renderCard();
|
||||
openAndFill(EMAIL, 'secret');
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/reac
|
||||
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import RequireSession from './RequireSession.jsx';
|
||||
import FinishedHint from './FinishedHint.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -24,9 +25,11 @@ const puts = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT')
|
||||
describe('FinishedHint', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async (url, init) => (String(url).endsWith('/finished-hint')
|
||||
? {ok: true, status: 204}
|
||||
: {ok: true, status: 200, json: async () => ({...JSON.parse(init.body), rating: null, notes: null, priority: null})}));
|
||||
fetchRoutes({
|
||||
'PUT /api/library/games/322110/status': (url, init) =>
|
||||
json(200, {...JSON.parse(init.body), rating: null, notes: null, priority: null}),
|
||||
'PUT /api/library/games/322110/finished-hint': json(204),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -83,7 +86,10 @@ describe('FinishedHint', () => {
|
||||
});
|
||||
|
||||
it('ends the session on a 401', async () => {
|
||||
globalThis.fetch = vi.fn(async () => ({ok: false, status: 401}));
|
||||
fetchRoutes({
|
||||
'PUT /api/library/games/322110/finished-hint': json(401, {}),
|
||||
'POST /api/auth/refresh': json(401, {}),
|
||||
});
|
||||
renderHint();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: 'GAME_DETAIL.NOT_YET'}));
|
||||
|
||||
@@ -4,6 +4,7 @@ import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/reac
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {toast} from 'sonner';
|
||||
import GameCorrectionDialog from './GameCorrectionDialog.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -14,7 +15,11 @@ const game = {
|
||||
corrections: [{field: 'NAME', correctedAt: '2026-09-28T10:00:00Z'}],
|
||||
};
|
||||
|
||||
const answer = (status, body = {}) => ({ok: status < 300, status, json: async () => body});
|
||||
const GAME_URL = '/api/library/games/620/corrections';
|
||||
const corrections = (answer) => fetchRoutes({
|
||||
[`PUT ${GAME_URL}/name`]: answer, [`PUT ${GAME_URL}/image`]: answer, [`PUT ${GAME_URL}/speedrun`]: answer,
|
||||
[`PUT ${GAME_URL}/moods`]: answer, [`DELETE ${GAME_URL}/name`]: answer,
|
||||
});
|
||||
|
||||
const renderDialog = (onChanged = vi.fn()) => {
|
||||
render(<MemoryRouter><GameCorrectionDialog game={game} open onOpenChange={vi.fn()} onChanged={onChanged}/></MemoryRouter>);
|
||||
@@ -28,7 +33,7 @@ const sent = () => fetch.mock.calls.map(([url, init]) => ({url, method: init?.me
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async () => answer(204));
|
||||
corrections(json(204, {}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -75,7 +80,7 @@ describe('GameCorrectionDialog', () => {
|
||||
});
|
||||
|
||||
it('shows why Core refused a value', async () => {
|
||||
globalThis.fetch = vi.fn(async () => answer(400, {code: 'INVALID_CORRECTION', message: 'image must be https'}));
|
||||
corrections(json(400, {code: 'INVALID_CORRECTION', message: 'image must be https'}));
|
||||
const onChanged = renderDialog();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('CORRECTION.FIELD.IMAGE'), {target: {value: 'http://x'}});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/reac
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {toast} from 'sonner';
|
||||
import GameMergeDialog from './GameMergeDialog.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -12,12 +13,11 @@ const hades = {appid: 1145360, name: 'Hades'};
|
||||
const hit = (appid, name) => ({appid, igdbId: Math.abs(appid), name, year: 2020, imageUrl: null,
|
||||
platforms: ['Switch'], inLibrary: false, inCatalog: true});
|
||||
|
||||
const json = (status, body) => ({ok: status < 400, status, json: async () => body});
|
||||
|
||||
function renderDialog(game, post, results) {
|
||||
globalThis.fetch = vi.fn(async (url, init) => init?.method === 'POST'
|
||||
? post
|
||||
: json(200, {available: true, results}));
|
||||
fetchRoutes({
|
||||
'GET /api/library/game-search': json(200, {available: true, results}),
|
||||
[`POST /api/library/games/${game.appid}/merge`]: post,
|
||||
});
|
||||
const onMerged = vi.fn();
|
||||
render(<MemoryRouter><GameMergeDialog game={game} open onOpenChange={vi.fn()} onMerged={onMerged}/></MemoryRouter>);
|
||||
return onMerged;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import PlayingHint from './PlayingHint.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
@@ -13,7 +14,7 @@ const renderHint = (props) => render(<MemoryRouter><PlayingHint game={game} onCh
|
||||
describe('PlayingHint', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async (url, init) => ({ok: true, status: 200, json: async () => JSON.parse(init.body)}));
|
||||
fetchRoutes({'PUT /api/library/games/220/backlog': (url, init) => json(200, JSON.parse(init.body))});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in new issue
Block a user