Public Access
105 lines
3.9 KiB
React
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);
|
|
});
|
|
});
|