Files
ToBePlayed-Frontend/src/pages/GameLibrary.test.jsx
T

105 lines
3.9 KiB
React

// @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 GameLibrary from './GameLibrary.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 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 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.
function renderLibrary(initialEntry = '/library') {
const lib = {navigations: 0, location: null, navigate: null};
function Spy() {
const location = useLocation();
lib.navigate = useNavigate();
if (lib.location?.key !== location.key) {
lib.navigations++;
lib.location = location;
}
return null;
}
render(
<MemoryRouter initialEntries={[initialEntry]}>
<Routes><Route path="/library" element={<><GameLibrary/><Spy/></>}/></Routes>
</MemoryRouter>,
);
return {
navigations: () => lib.navigations,
params: () => new URLSearchParams(lib.location.search),
back: () => lib.navigate(-1),
};
}
const typeInto = (value) => fireEvent.change(screen.getByRole('searchbox'), {target: {value}});
beforeEach(() => {
localStorage.setItem('tbp_jwt', 'token');
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => page}));
});
afterEach(() => {
cleanup();
localStorage.clear();
});
describe('GameLibrary search', () => {
it('writes the trimmed search to the URL after the delay and resets the page', async () => {
const lib = renderLibrary('/library?page=3');
typeInto('half');
await waitFor(() => expect(lib.params().get('q')).toBe('half'));
expect(lib.params().get('page')).toBe('0');
await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/[?&]q=half(&|$)/));
});
it('does not keep navigating and keeps a trailing space while the user types', async () => {
const lib = renderLibrary();
typeInto('half');
await waitFor(() => expect(lib.params().get('q')).toBe('half'));
const navigationsBefore = lib.navigations();
typeInto('half ');
await wait(DEBOUNCE_WAIT_MS);
expect(lib.navigations()).toBe(navigationsBefore);
expect(screen.getByRole('searchbox').value).toBe('half ');
});
it('keeps a status picked during the delay', async () => {
const lib = renderLibrary();
typeInto('half');
fireEvent.click(screen.getByRole('button', {name: 'BACKLOG.STATUS.DROPPED'}));
await waitFor(() => expect(lib.params().get('q')).toBe('half'));
expect(lib.params().get('status')).toBe('DROPPED');
});
it('shows the URL search again after back navigation', async () => {
const lib = renderLibrary();
typeInto('half');
await waitFor(() => expect(lib.params().get('q')).toBe('half'));
act(() => lib.back());
await waitFor(() => expect(screen.getByRole('searchbox').value).toBe(''));
});
});
describe('GameLibrary sort', () => {
it.each(['rating,desc', 'priority,desc'])('sends the sort %s from the URL', async (sort) => {
renderLibrary(`/library?sort=${encodeURIComponent(sort)}`);
await waitFor(() => expect(fetch).toHaveBeenCalled());
expect(new URL(fetch.mock.calls[0][0], 'http://x').searchParams.get('sort')).toBe(sort);
});
});