Public Access
feat(library): add platform badges, remove hide duplicates
Gitea #34 slice 2 of 3. Core lists each set once now. The hide duplicates toggle and its param are not needed. PlatformIcons shows a badge per platform (platforms[]).
This commit is contained in:
1 parent
25cc5ff2d8
commit
16651a1f56
13 files changed
+120
-48
No files matched your search
@@ -55,12 +55,14 @@ To Be Played is split into three repositories:
|
|||||||
system setting "reduce motion" or the "Reduce animations" switch in the settings.
|
system setting "reduce motion" or the "Reduce animations" switch in the settings.
|
||||||
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
|
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
|
||||||
finished games per year of all users.
|
finished games per year of all users.
|
||||||
- **Library**: status filters, source filter (Steam, PlayStation, manual; a linked set counts on each of its sources), "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
|
- **Library**: status filters, source filter (Steam, PlayStation, manual; a linked set matches on each of its sources), "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
|
||||||
in library, rating or priority, paging. Each row shows the unlocked achievements.
|
in library, rating or priority, paging. Each row shows the unlocked achievements. A game you own on several
|
||||||
|
platforms is one row (Core's counted edition) with a small badge per platform next to the title (`PlatformIcons`:
|
||||||
|
ST, PS or the platform name, from `platforms[]`; lucide has no brand icons).
|
||||||
- **Editions**: the editions of one game (Steam and PS5 copy, remaster, port) are linked automatically from IGDB's
|
- **Editions**: the editions of one game (Steam and PS5 copy, remaster, port) are linked automatically from IGDB's
|
||||||
"same game" key; an auditor can also link or unlink them for every user. A user who owns two or more of a linked
|
"same game" key; an auditor can also link or unlink them for every user. A user who owns two or more of a linked
|
||||||
set sees "Also in your library" on the details page, an "x2" badge in the library and the filter "Hide
|
set sees "Also in your library" on the details page and an "x2" badge in the library; the library lists the set
|
||||||
duplicates"; the numbers count the set once. The editions of a set share one status, rating and notes (the page
|
once (no "Hide duplicates" filter any more), and the numbers count it once. The editions of a set share one status, rating and notes (the page
|
||||||
reloads after a save to show it). "Not the same game" splits the game off its set for this user only, and it stays
|
reloads after a save to show it). "Not the same game" splits the game off its set for this user only, and it stays
|
||||||
split. "Same game as..." on the details page asks the auditors for a link.
|
split. "Same game as..." on the details page asks the auditors for a link.
|
||||||
- **Add a game by name** for games outside Steam (PlayStation, Switch, Xbox, physical): search IGDB, pick a hit,
|
- **Add a game by name** for games outside Steam (PlayStation, Switch, Xbox, physical): search IGDB, pick a hit,
|
||||||
@@ -259,6 +261,13 @@ flowchart LR
|
|||||||
which question is open; `src/utils/questions.js` only loads the list and sends the answer. A yes to Finished? gets
|
which question is open; `src/utils/questions.js` only loads the list and sends the answer. A yes to Finished? gets
|
||||||
the Rate it? card back at once. `409` / `404` reload the list. `RatingStars` is shared with `BacklogCard`.
|
the Rate it? card back at once. `409` / `404` reload the list. `RatingStars` is shared with `BacklogCard`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Games["Core GET /api/library/games: one row per set + platforms[]"] --> Row["GameLibrary row"]
|
||||||
|
Row --> Icons["PlatformIcons: badge per platform"]
|
||||||
|
Row -.->|"no platforms (hidden game)"| Badge["single platform badge"]
|
||||||
|
```
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
Detail["GameDetail: EditionsCard"] -->|"Not the same game"| Split["Core DELETE /api/library/games/{appid}/set"]
|
Detail["GameDetail: EditionsCard"] -->|"Not the same game"| Split["Core DELETE /api/library/games/{appid}/set"]
|
||||||
|
|||||||
@@ -291,7 +291,6 @@
|
|||||||
"FILTER_HIDDEN": "Ausgeblendet",
|
"FILTER_HIDDEN": "Ausgeblendet",
|
||||||
"NO_HIDDEN": "Keine ausgeblendeten Spiele. Blende ein Spiel auf seiner Detailseite aus, z. B. einen Playtest oder ein Tool.",
|
"NO_HIDDEN": "Keine ausgeblendeten Spiele. Blende ein Spiel auf seiner Detailseite aus, z. B. einen Playtest oder ein Tool.",
|
||||||
"ACHIEVEMENTS": "{{unlocked}} von {{total}} Erfolgen",
|
"ACHIEVEMENTS": "{{unlocked}} von {{total}} Erfolgen",
|
||||||
"FILTER_HIDE_DUPLICATES": "Duplikate ausblenden",
|
|
||||||
"EDITIONS": "{{count}} Ausgaben in deiner Bibliothek"
|
"EDITIONS": "{{count}} Ausgaben in deiner Bibliothek"
|
||||||
},
|
},
|
||||||
"GAME_DETAIL": {
|
"GAME_DETAIL": {
|
||||||
|
|||||||
@@ -291,7 +291,6 @@
|
|||||||
"FILTER_HIDDEN": "Hidden",
|
"FILTER_HIDDEN": "Hidden",
|
||||||
"NO_HIDDEN": "No hidden games. Hide a game on its detail page, e.g. a playtest or a tool.",
|
"NO_HIDDEN": "No hidden games. Hide a game on its detail page, e.g. a playtest or a tool.",
|
||||||
"ACHIEVEMENTS": "{{unlocked}} of {{total}} achievements",
|
"ACHIEVEMENTS": "{{unlocked}} of {{total}} achievements",
|
||||||
"FILTER_HIDE_DUPLICATES": "Hide duplicates",
|
|
||||||
"EDITIONS": "{{count}} editions in your library"
|
"EDITIONS": "{{count}} editions in your library"
|
||||||
},
|
},
|
||||||
"GAME_DETAIL": {
|
"GAME_DETAIL": {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('PileOfShameCard', () => {
|
|||||||
render(<MemoryRouter><StatDrillProvider source="own"><PileOfShameCard summary={summary(2)} onShuffle={() => {}}/></StatDrillProvider></MemoryRouter>);
|
render(<MemoryRouter><StatDrillProvider source="own"><PileOfShameCard summary={summary(2)} onShuffle={() => {}}/></StatDrillProvider></MemoryRouter>);
|
||||||
|
|
||||||
expect(screen.getAllByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 2'})[0].getAttribute('href'))
|
expect(screen.getAllByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 2'})[0].getAttribute('href'))
|
||||||
.toBe('/library?hideDuplicates=true&status=BACKLOG');
|
.toBe('/library?status=BACKLOG');
|
||||||
fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.NEVER_PLAYED:1'}));
|
fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.NEVER_PLAYED:1'}));
|
||||||
|
|
||||||
expect(await screen.findByText('Untouched')).toBeTruthy();
|
expect(await screen.findByText('Untouched')).toBeTruthy();
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {useTranslation} from 'react-i18next';
|
||||||
|
|
||||||
|
import {platformLabel} from '../utils/platforms.js';
|
||||||
|
|
||||||
|
// lucide has no brand icons: a short text badge per platform.
|
||||||
|
const shortName = ({source, platform}) => {
|
||||||
|
if (source === 'STEAM') return 'ST';
|
||||||
|
return platform ?? (source === 'PSN' ? 'PS' : 'M');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param platforms the {source, platform} of every edition of a library entry, see Core's `platforms`
|
||||||
|
*/
|
||||||
|
export default function PlatformIcons({platforms}) {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
if (!platforms?.length) return null;
|
||||||
|
return (
|
||||||
|
<ul className="flex flex-wrap gap-1">
|
||||||
|
{platforms.map((p) => {
|
||||||
|
const label = platformLabel(p) ?? t(`LIBRARY.SOURCE.${p.source}`);
|
||||||
|
return (
|
||||||
|
<li key={`${p.source}:${p.platform ?? ''}`} title={label} aria-label={label}
|
||||||
|
className="rounded border px-1 text-[10px] leading-4 font-semibold text-muted-foreground">
|
||||||
|
{shortName(p)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||||
|
import {cleanup, render, screen} from '@testing-library/react';
|
||||||
|
import PlatformIcons from './PlatformIcons.jsx';
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
describe('PlatformIcons', () => {
|
||||||
|
it('showsOneBadgeWithALabelPerPlatform', () => {
|
||||||
|
render(<PlatformIcons platforms={[{source: 'STEAM', platform: null}, {source: 'PSN', platform: 'PS5'}]}/>);
|
||||||
|
|
||||||
|
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
||||||
|
expect(screen.getByLabelText('Steam').textContent).toBe('ST');
|
||||||
|
expect(screen.getByLabelText('PS5').textContent).toBe('PS5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('namesAPlayStationTitleWithoutPlatformAfterItsSource', () => {
|
||||||
|
render(<PlatformIcons platforms={[{source: 'PSN', platform: null}]}/>);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('LIBRARY.SOURCE.PSN').textContent).toBe('PS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showsAManualPlatformByName', () => {
|
||||||
|
render(<PlatformIcons platforms={[{source: 'MANUAL', platform: 'Switch'}]}/>);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('Switch').textContent).toBe('Switch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aManualGameWithoutPlatformIsNotShownAsPlayStation', () => {
|
||||||
|
render(<PlatformIcons platforms={[{source: 'MANUAL', platform: null}]}/>);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('LIBRARY.SOURCE.MANUAL').textContent).not.toBe('PS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rendersNothingWithoutPlatforms', () => {
|
||||||
|
const {container} = render(<PlatformIcons platforms={[]}/>);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
cleanup();
|
||||||
|
const missing = render(<PlatformIcons/>);
|
||||||
|
expect(missing.container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
+11
-18
@@ -23,6 +23,7 @@ import LibraryTabs from '../components/LibraryTabs.jsx';
|
|||||||
import ComingBadge from '../components/ComingBadge.jsx';
|
import ComingBadge from '../components/ComingBadge.jsx';
|
||||||
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
||||||
import PageContainer from '../components/PageContainer.jsx';
|
import PageContainer from '../components/PageContainer.jsx';
|
||||||
|
import PlatformIcons from '../components/PlatformIcons.jsx';
|
||||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||||
import {coverEffect} from '../utils/rewards.js';
|
import {coverEffect} from '../utils/rewards.js';
|
||||||
import {apiFetch, request} from '../utils/api.js';
|
import {apiFetch, request} from '../utils/api.js';
|
||||||
@@ -58,15 +59,13 @@ export default function GameLibrary() {
|
|||||||
const q = searchParams.get('q') ?? '';
|
const q = searchParams.get('q') ?? '';
|
||||||
const shortOnly = searchParams.get('maxMinutes') === String(SHORT_GAME_MINUTES);
|
const shortOnly = searchParams.get('maxMinutes') === String(SHORT_GAME_MINUTES);
|
||||||
const hiddenOnly = searchParams.get('hidden') === 'true';
|
const hiddenOnly = searchParams.get('hidden') === 'true';
|
||||||
const hideDuplicates = searchParams.get('hideDuplicates') === 'true';
|
|
||||||
// The hidden list stands alone, also for a hand-made URL: its status and short filters are ignored.
|
// The hidden list stands alone, also for a hand-made URL: its status and short filters are ignored.
|
||||||
const listStatus = hiddenOnly ? null : status;
|
const listStatus = hiddenOnly ? null : status;
|
||||||
const listShortOnly = !hiddenOnly && shortOnly;
|
const listShortOnly = !hiddenOnly && shortOnly;
|
||||||
const listHideDuplicates = !hiddenOnly && hideDuplicates;
|
|
||||||
const sources = parseSources(searchParams.get('source'));
|
const sources = parseSources(searchParams.get('source'));
|
||||||
const listSources = hiddenOnly ? [] : sources;
|
const listSources = hiddenOnly ? [] : sources;
|
||||||
const sourceKey = listSources.join(',');
|
const sourceKey = listSources.join(',');
|
||||||
const filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly || listHideDuplicates || sourceKey);
|
const filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly || sourceKey);
|
||||||
const [search, setSearch] = useState(q);
|
const [search, setSearch] = useState(q);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
// A Steam ID or a PlayStation link. Starts true until /games-count answers, so the sync controls do not vanish
|
// A Steam ID or a PlayStation link. Starts true until /games-count answers, so the sync controls do not vanish
|
||||||
@@ -83,7 +82,7 @@ export default function GameLibrary() {
|
|||||||
const signal = nextSignal();
|
const signal = nextSignal();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status: listStatus, q, maxMinutes: listShortOnly ? SHORT_GAME_MINUTES : null, hidden: hiddenOnly, hideDuplicates: listHideDuplicates, source: sourceKey})}`;
|
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status: listStatus, q, maxMinutes: listShortOnly ? SHORT_GAME_MINUTES : null, hidden: hiddenOnly, source: sourceKey})}`;
|
||||||
const {ok, status, data: json} = await request(url, {signal});
|
const {ok, status, data: json} = await request(url, {signal});
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -93,7 +92,7 @@ export default function GameLibrary() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData(json);
|
setData(json);
|
||||||
}, [nextSignal, page, size, sort, listStatus, q, listShortOnly, hiddenOnly, listHideDuplicates, sourceKey]);
|
}, [nextSignal, page, size, sort, listStatus, q, listShortOnly, hiddenOnly, sourceKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
@@ -158,7 +157,7 @@ export default function GameLibrary() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onHiddenChange = () => {
|
const onHiddenChange = () => {
|
||||||
updateParams(hiddenOnly ? {hidden: null, page: 0} : {hidden: true, status: null, maxMinutes: null, hideDuplicates: null, source: null, page: 0});
|
updateParams(hiddenOnly ? {hidden: null, page: 0} : {hidden: true, status: null, maxMinutes: null, source: null, page: 0});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSizeChange = (value) => {
|
const onSizeChange = (value) => {
|
||||||
@@ -261,15 +260,6 @@ export default function GameLibrary() {
|
|||||||
>
|
>
|
||||||
<Timer className="size-3"/>{t('LIBRARY.FILTER_SHORT')}
|
<Timer className="size-3"/>{t('LIBRARY.FILTER_SHORT')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={listHideDuplicates ? 'default' : 'outline'}
|
|
||||||
aria-pressed={listHideDuplicates}
|
|
||||||
className="h-7 rounded-full px-3 text-xs"
|
|
||||||
onClick={() => updateParams({hideDuplicates: listHideDuplicates ? null : true, hidden: null, page: 0})}
|
|
||||||
>
|
|
||||||
<Layers className="size-3"/>{t('LIBRARY.FILTER_HIDE_DUPLICATES')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={hiddenOnly ? 'default' : 'outline'}
|
variant={hiddenOnly ? 'default' : 'outline'}
|
||||||
@@ -346,10 +336,13 @@ export default function GameLibrary() {
|
|||||||
>
|
>
|
||||||
<GameCover appid={g.appid} imageUrl={g.imageUrl} fx={coverEffect(equipped, g.status)}
|
<GameCover appid={g.appid} imageUrl={g.imageUrl} fx={coverEffect(equipped, g.status)}
|
||||||
className="w-16 sm:w-24 shrink-0"/>
|
className="w-16 sm:w-24 shrink-0"/>
|
||||||
<span className="@container min-w-0 flex-1 font-medium group-hover:text-primary transition-colors">
|
<span className="@container min-w-0 flex-1">
|
||||||
<FitText text={g.name ?? `App ${g.appid}`}/>
|
<span className="block font-medium group-hover:text-primary transition-colors">
|
||||||
|
<FitText text={g.name ?? `App ${g.appid}`}/>
|
||||||
|
</span>
|
||||||
|
<PlatformIcons platforms={g.platforms}/>
|
||||||
</span>
|
</span>
|
||||||
{platformLabel(g) && (
|
{!g.platforms?.length && platformLabel(g) && (
|
||||||
<Badge variant="outline" className="hidden sm:inline-flex" title={platformLabel(g)}>
|
<Badge variant="outline" className="hidden sm:inline-flex" title={platformLabel(g)}>
|
||||||
<Gamepad2 className="size-3"/>{platformLabel(g)}
|
<Gamepad2 className="size-3"/>{platformLabel(g)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -217,15 +217,22 @@ describe('GameLibrary short games', () => {
|
|||||||
expect(screen.getAllByText(/^x\d/)).toHaveLength(1);
|
expect(screen.getAllByText(/^x\d/)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hideDuplicatesFilter_AddsHideDuplicatesToTheUrlAndTheRequest', async () => {
|
it('hasNoHideDuplicatesToggle_AndTheRequestNeverCarriesTheParam', async () => {
|
||||||
const lib = renderLibrary();
|
renderLibrary('/library?hideDuplicates=true');
|
||||||
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'}));
|
expect(screen.queryByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'})).toBeNull();
|
||||||
|
expect(fetch.mock.calls.every(([url]) => !String(url).includes('hideDuplicates'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(lib.params().get('hideDuplicates')).toBe('true'));
|
it('showsThePlatformsOfAnEntryInsteadOfTheSinglePlatformBadge', async () => {
|
||||||
await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/[?&]hideDuplicates=true(&|$)/));
|
answer([{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG', source: 'STEAM', platform: null,
|
||||||
expect(screen.getByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'}).getAttribute('aria-pressed')).toBe('true');
|
platforms: [{source: 'STEAM', platform: null}, {source: 'PSN', platform: 'PS5'}]}], true);
|
||||||
|
renderLibrary();
|
||||||
|
|
||||||
|
expect(await screen.findByLabelText('PS5')).toBeTruthy();
|
||||||
|
expect(screen.getByLabelText('Steam')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('Steam')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leaves the hidden games with a status click', async () => {
|
it('leaves the hidden games with a status click', async () => {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('StatsPage', () => {
|
|||||||
|
|
||||||
const link = await screen.findByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/});
|
const link = await screen.findByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/});
|
||||||
|
|
||||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true&status=BACKLOG');
|
expect(link.getAttribute('href')).toBe('/library?status=BACKLOG');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('aZeroTileIsNotClickable', async () => {
|
it('aZeroTileIsNotClickable', async () => {
|
||||||
@@ -178,7 +178,7 @@ describe('StatsPage', () => {
|
|||||||
const link = await within(await screen.findByText('STATS.OVERVIEW.TITLE').then(overview))
|
const link = await within(await screen.findByText('STATS.OVERVIEW.TITLE').then(overview))
|
||||||
.findByRole('link', {name: 'STATS.OVERVIEW.GAMES: 12'});
|
.findByRole('link', {name: 'STATS.OVERVIEW.GAMES: 12'});
|
||||||
|
|
||||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true');
|
expect(link.getAttribute('href')).toBe('/library');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('linksALegendRowToTheStatusLibrary', async () => {
|
it('linksALegendRowToTheStatusLibrary', async () => {
|
||||||
@@ -188,7 +188,7 @@ describe('StatsPage', () => {
|
|||||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||||
|
|
||||||
expect(within(overview()).getByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/}).getAttribute('href'))
|
expect(within(overview()).getByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/}).getAttribute('href'))
|
||||||
.toBe('/library?hideDuplicates=true&status=BACKLOG');
|
.toBe('/library?status=BACKLOG');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leavesAStatusWithoutGamesWithoutSliceAndTarget', async () => {
|
it('leavesAStatusWithoutGamesWithoutSliceAndTarget', async () => {
|
||||||
@@ -211,7 +211,7 @@ describe('StatsPage', () => {
|
|||||||
fireEvent.click(paths()[1]);
|
fireEvent.click(paths()[1]);
|
||||||
|
|
||||||
await waitFor(() => expect(router.state.location.pathname + router.state.location.search)
|
await waitFor(() => expect(router.state.location.pathname + router.state.location.search)
|
||||||
.toBe('/library?hideDuplicates=true&status=PLAYING'));
|
.toBe('/library?status=PLAYING'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('showsTheGamesTileInsteadOfTheDonutForAnEmptyLibrary', async () => {
|
it('showsTheGamesTileInsteadOfTheDonutForAnEmptyLibrary', async () => {
|
||||||
|
|||||||
@@ -58,10 +58,9 @@ export const dismissFinishedHint = (appid, dismissed) =>
|
|||||||
body: JSON.stringify({dismissed}),
|
body: JSON.stringify({dismissed}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const libraryQuery = ({page, size, sort, status, q, maxMinutes, hidden, hideDuplicates, source}) => {
|
export const libraryQuery = ({page, size, sort, status, q, maxMinutes, hidden, source}) => {
|
||||||
const params = new URLSearchParams({page: String(page), size: String(size), sort});
|
const params = new URLSearchParams({page: String(page), size: String(size), sort});
|
||||||
if (hidden) params.set('hidden', 'true');
|
if (hidden) params.set('hidden', 'true');
|
||||||
if (hideDuplicates) params.set('hideDuplicates', 'true');
|
|
||||||
if (status) params.set('status', status);
|
if (status) params.set('status', status);
|
||||||
if (q?.trim()) params.set('q', q.trim());
|
if (q?.trim()) params.set('q', q.trim());
|
||||||
if (maxMinutes) params.set('maxMinutes', String(maxMinutes));
|
if (maxMinutes) params.set('maxMinutes', String(maxMinutes));
|
||||||
|
|||||||
@@ -53,14 +53,6 @@ describe('backlogShare', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('libraryQuery hideDuplicates', () => {
|
|
||||||
it('libraryQuery_SetsHideDuplicatesOnlyWhenAsked', () => {
|
|
||||||
const base = {page: 0, size: 20, sort: 'appid,asc', status: null, q: ''};
|
|
||||||
expect(libraryQuery({...base, hideDuplicates: true})).toBe('page=0&size=20&sort=appid%2Casc&hideDuplicates=true');
|
|
||||||
expect(libraryQuery({...base, hideDuplicates: false})).not.toContain('hideDuplicates');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sources', () => {
|
describe('sources', () => {
|
||||||
it('parseSources_KeepsKnownValuesInOrder', () => {
|
it('parseSources_KeepsKnownValuesInOrder', () => {
|
||||||
expect(parseSources('PSN,FOO,STEAM,PSN')).toEqual(['STEAM', 'PSN']);
|
expect(parseSources('PSN,FOO,STEAM,PSN')).toEqual(['STEAM', 'PSN']);
|
||||||
|
|||||||
@@ -45,6 +45,6 @@ export async function fetchStatGames(source, params, ticket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const libraryLink = (status) => `/library?hideDuplicates=true${status ? `&status=${status}` : ''}`;
|
export const libraryLink = (status) => `/library${status ? `?status=${status}` : ''}`;
|
||||||
|
|
||||||
export const gameHref = (appid) => `/library/${appid}`;
|
export const gameHref = (appid) => `/library/${appid}`;
|
||||||
@@ -21,8 +21,8 @@ describe('statGamesUrl', () => {
|
|||||||
|
|
||||||
describe('libraryLink', () => {
|
describe('libraryLink', () => {
|
||||||
it('addsTheStatusOnlyWhenGiven', () => {
|
it('addsTheStatusOnlyWhenGiven', () => {
|
||||||
expect(libraryLink()).toBe('/library?hideDuplicates=true');
|
expect(libraryLink()).toBe('/library');
|
||||||
expect(libraryLink('BACKLOG')).toBe('/library?hideDuplicates=true&status=BACKLOG');
|
expect(libraryLink('BACKLOG')).toBe('/library?status=BACKLOG');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user