Public Access
feat(library): add source chips to game library
Chips show only when the user has 2 or more sources. A selected source the user lacks stays visible, so it can be cleared.
This commit is contained in:
1 parent
d3ad19e2fa
commit
e136a7abc1
8 files changed
+224
-55
No files matched your search
@@ -1,4 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import {StrictMode} from 'react';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {act, cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {createMemoryRouter, RouterProvider, useLocation, useNavigate} from 'react-router-dom';
|
||||
@@ -11,16 +12,20 @@ import {toast} from 'sonner';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
vi.mock('../hooks/useLibrarySync.js', () => ({default: () => ({status: 'OK', lastSyncedAt: null, isRunning: false})}));
|
||||
const syncOptions = vi.hoisted(() => ({current: null}));
|
||||
vi.mock('../hooks/useLibrarySync.js', () => ({default: (opts) => {
|
||||
syncOptions.current = opts;
|
||||
return {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 answer = (games = page.content, canSync = true, hasSteamId = canSync) => fetchRoutes({
|
||||
const answer = (games = page.content, canSync = true, hasSteamId = canSync, sources = []) => fetchRoutes({
|
||||
'GET /api/users/me': json(200, {settings: {}}),
|
||||
'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, canSync}),
|
||||
'GET /api/library/games-count': json(200, {count: games.length, hasSteamId, canSync, sources}),
|
||||
});
|
||||
|
||||
const wait = (ms) => act(() => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
@@ -250,6 +255,106 @@ describe('GameLibrary short games', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameLibrary source filter', () => {
|
||||
const lastGamesUrl = () => String(fetch.mock.calls.map(([url]) => url).filter((url) => String(url).includes('/api/library/games?')).at(-1));
|
||||
const chip = (source) => screen.getByRole('button', {name: `LIBRARY.SOURCE.${source}`});
|
||||
|
||||
it('sourceFilter_AddsSourceToTheUrlAndTheRequest', async () => {
|
||||
answer(page.content, true, true, ['STEAM', 'PSN']);
|
||||
const lib = renderLibrary('/library?page=2');
|
||||
await screen.findByRole('group', {name: 'LIBRARY.SOURCE_LABEL'});
|
||||
|
||||
fireEvent.click(chip('STEAM'));
|
||||
await waitFor(() => expect(lib.params().get('source')).toBe('STEAM'));
|
||||
fireEvent.click(chip('PSN'));
|
||||
|
||||
await waitFor(() => expect(lib.params().get('source')).toBe('STEAM,PSN'));
|
||||
expect(lib.params().get('page')).toBe('0');
|
||||
await waitFor(() => expect(lastGamesUrl()).toMatch(/[?&]source=STEAM%2CPSN(&|$)/));
|
||||
expect(chip('STEAM').getAttribute('aria-pressed')).toBe('true');
|
||||
fireEvent.click(chip('STEAM'));
|
||||
await waitFor(() => expect(lib.params().get('source')).toBe('PSN'));
|
||||
fireEvent.click(chip('PSN'));
|
||||
await waitFor(() => expect(lib.params().get('source')).toBeNull());
|
||||
});
|
||||
|
||||
it('sourceFilter_HiddenWithOneOwnSource', async () => {
|
||||
answer(page.content, true, true, ['STEAM']);
|
||||
renderLibrary();
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||
await wait(50);
|
||||
|
||||
expect(screen.queryByRole('group', {name: 'LIBRARY.SOURCE_LABEL'})).toBeNull();
|
||||
});
|
||||
|
||||
it('sourceFilter_ShowsASelectedSourceTheUserLacks', async () => {
|
||||
answer(page.content, true, true, ['STEAM']);
|
||||
const lib = renderLibrary('/library?source=PSN');
|
||||
|
||||
expect((await screen.findByRole('button', {name: 'LIBRARY.SOURCE.PSN'})).getAttribute('aria-pressed')).toBe('true');
|
||||
fireEvent.click(chip('PSN'));
|
||||
await waitFor(() => expect(lib.params().get('source')).toBeNull());
|
||||
});
|
||||
|
||||
it('sourceFilter_IgnoresUnknownValuesInTheUrl', async () => {
|
||||
answer(page.content, true, true, ['STEAM', 'PSN']);
|
||||
renderLibrary('/library?source=XBOX');
|
||||
|
||||
await waitFor(() => expect(lastGamesUrl()).toContain('/api/library/games?'));
|
||||
expect(lastGamesUrl()).not.toMatch(/source=/);
|
||||
});
|
||||
|
||||
it('hiddenFilter_ClearsTheSource', async () => {
|
||||
answer(page.content, true, true, ['STEAM', 'PSN']);
|
||||
const lib = renderLibrary('/library?source=PSN');
|
||||
await screen.findByRole('group', {name: 'LIBRARY.SOURCE_LABEL'});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: 'LIBRARY.FILTER_HIDDEN'}));
|
||||
|
||||
await waitFor(() => expect(lib.params().get('hidden')).toBe('true'));
|
||||
expect(lib.params().get('source')).toBeNull();
|
||||
await waitFor(() => expect(lastGamesUrl()).not.toMatch(/source=/));
|
||||
});
|
||||
|
||||
it('hiddenLink_IgnoresTheSource', async () => {
|
||||
renderLibrary('/library?hidden=true&source=PSN');
|
||||
|
||||
await waitFor(() => expect(lastGamesUrl()).toMatch(/[?&]hidden=true(&|$)/));
|
||||
expect(lastGamesUrl()).not.toMatch(/source=/);
|
||||
});
|
||||
|
||||
it('sourceFilter_ChipsFollowAFinishedSync', async () => {
|
||||
answer(page.content, true, true, ['STEAM']);
|
||||
renderLibrary();
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||
await wait(50);
|
||||
expect(screen.queryByRole('group', {name: 'LIBRARY.SOURCE_LABEL'})).toBeNull();
|
||||
|
||||
answer(page.content, true, true, ['STEAM', 'PSN']);
|
||||
await act(async () => syncOptions.current.onComplete());
|
||||
|
||||
expect(await screen.findByRole('group', {name: 'LIBRARY.SOURCE_LABEL'})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sourceFilter_ChipsShowUnderStrictMode', async () => {
|
||||
answer(page.content, true, true, ['STEAM', 'PSN']);
|
||||
const router = createMemoryRouter([{
|
||||
path: '/library',
|
||||
element: <PreferencesProvider><CurrentUserProvider><GameLibrary/></CurrentUserProvider></PreferencesProvider>,
|
||||
}], {initialEntries: ['/library']});
|
||||
render(<StrictMode><RouterProvider router={router}/></StrictMode>);
|
||||
|
||||
expect(await screen.findByRole('group', {name: 'LIBRARY.SOURCE_LABEL'})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sourceFilter_EmptyResultShowsNoMatch', async () => {
|
||||
answer([], true, true, ['STEAM', 'PSN']);
|
||||
renderLibrary('/library?source=PSN');
|
||||
|
||||
expect(await screen.findByText('LIBRARY.NO_MATCH')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameLibrary with games added by hand', () => {
|
||||
const zelda = {appid: -1025, name: 'Zelda', playtimeMinutes: 90, status: 'BACKLOG', source: 'MANUAL',
|
||||
platform: 'Nintendo Switch', imageUrl: 'https://images.igdb.com/z.jpg'};
|
||||
|
||||
Reference in new issue
Block a user