Public Access
feat(games): add coming badge for unreleased games
This commit is contained in:
1 parent
edeea5a43d
commit
c158909083
17 files changed
+169
-3
No files matched your search
@@ -25,6 +25,9 @@ To Be Played is split into three repositories:
|
|||||||
days ("this month: +N added, -N off the pile") and a random suggestion,
|
days ("this month: +N added, -N off the pile") and a random suggestion,
|
||||||
Steam avatar and a banner from the most played game of the last two weeks, and an achievement card: unlocked
|
Steam avatar and a banner from the most played game of the last two weeks, and an achievement card: unlocked
|
||||||
total, perfect games, average completion and the 5 games closest to 100 %.
|
total, perfect games, average completion and the 5 games closest to 100 %.
|
||||||
|
- **Coming games**: a game that is not released yet (and nobody played) shows a "Coming <date>" badge in the library, on
|
||||||
|
the details page, in the stat game lists and on the public profile. The pile counts only the games Core sends in
|
||||||
|
`pileGames`, so coming games are not on it; Core never picks them.
|
||||||
- **What to play next** (`/play-next`): three backlog picks for a time budget (up to 5 / 15 / 40 h or any) and a mood
|
- **What to play next** (`/play-next`): three backlog picks for a time budget (up to 5 / 15 / 40 h or any) and a mood
|
||||||
(story, co-op, chill, action, scary from IGDB; speedrun from speedrun.com). Each pick says why: like a game you
|
(story, co-op, chill, action, scary from IGDB; speedrun from speedrun.com). Each pick says why: like a game you
|
||||||
played a lot, waiting longest, or random. Reroll without repeats, and "Start playing" in one click.
|
played a lot, waiting longest, or random. Reroll without repeats, and "Start playing" in one click.
|
||||||
|
|||||||
@@ -234,6 +234,8 @@
|
|||||||
"LOADING": "Lade Spiele …",
|
"LOADING": "Lade Spiele …",
|
||||||
"EMPTY": "Noch keine Spiele. Synchronisiere deine Steam-Bibliothek oder füge ein Spiel hinzu.",
|
"EMPTY": "Noch keine Spiele. Synchronisiere deine Steam-Bibliothek oder füge ein Spiel hinzu.",
|
||||||
"ERROR_LOAD": "Spiele konnten nicht geladen werden.",
|
"ERROR_LOAD": "Spiele konnten nicht geladen werden.",
|
||||||
|
"COMING": "Erscheint am {{date}}",
|
||||||
|
"COMING_SOON": "Erscheint bald",
|
||||||
"PER_PAGE": "Pro Seite:",
|
"PER_PAGE": "Pro Seite:",
|
||||||
"SORT_BY": "Sortieren:",
|
"SORT_BY": "Sortieren:",
|
||||||
"SORT_PLAYTIME_DESC": "Meistgespielt",
|
"SORT_PLAYTIME_DESC": "Meistgespielt",
|
||||||
|
|||||||
@@ -234,6 +234,8 @@
|
|||||||
"LOADING": "Loading games …",
|
"LOADING": "Loading games …",
|
||||||
"EMPTY": "No games yet. Sync your Steam library or add a game.",
|
"EMPTY": "No games yet. Sync your Steam library or add a game.",
|
||||||
"ERROR_LOAD": "Could not load your games.",
|
"ERROR_LOAD": "Could not load your games.",
|
||||||
|
"COMING": "Coming {{date}}",
|
||||||
|
"COMING_SOON": "Coming soon",
|
||||||
"PER_PAGE": "Per page:",
|
"PER_PAGE": "Per page:",
|
||||||
"SORT_BY": "Sort by:",
|
"SORT_BY": "Sort by:",
|
||||||
"SORT_PLAYTIME_DESC": "Most played",
|
"SORT_PLAYTIME_DESC": "Most played",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {useTranslation} from 'react-i18next';
|
||||||
|
import {CalendarClock} from 'lucide-react';
|
||||||
|
|
||||||
|
import {Badge} from '@/components/ui/badge';
|
||||||
|
import {formatDay} from '../utils/format.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "coming <date>" badge of a game that is not released yet; nothing for a released game. The date comes from the
|
||||||
|
* backend as `comingOn`, null for "coming soon".
|
||||||
|
*
|
||||||
|
* @param compact shows only the icon below the sm breakpoint
|
||||||
|
*/
|
||||||
|
export default function ComingBadge({game, compact = false}) {
|
||||||
|
const {t, i18n} = useTranslation();
|
||||||
|
if (!game.coming) return null;
|
||||||
|
const label = game.comingOn
|
||||||
|
? t('LIBRARY.COMING', {date: formatDay(game.comingOn, i18n.language, {dateStyle: 'medium'})})
|
||||||
|
: t('LIBRARY.COMING_SOON');
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" title={label}>
|
||||||
|
<CalendarClock/>
|
||||||
|
<span className={compact ? 'hidden sm:inline' : undefined}>{label}</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||||
|
import {cleanup, render, screen} from '@testing-library/react';
|
||||||
|
import ComingBadge from './ComingBadge.jsx';
|
||||||
|
|
||||||
|
const i18n = vi.hoisted(() => ({language: 'en'}));
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key, options) => (options?.date ? `${key} ${options.date}` : key),
|
||||||
|
i18n,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ComingBadge', () => {
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
it('shows the release date in English', () => {
|
||||||
|
i18n.language = 'en';
|
||||||
|
render(<ComingBadge game={{coming: true, comingOn: '2026-12-04'}}/>);
|
||||||
|
|
||||||
|
expect(screen.getByText('LIBRARY.COMING Dec 4, 2026')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the release date in German', () => {
|
||||||
|
i18n.language = 'de';
|
||||||
|
render(<ComingBadge game={{coming: true, comingOn: '2026-12-04'}}/>);
|
||||||
|
|
||||||
|
expect(screen.getByText('LIBRARY.COMING 04.12.2026')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('says "coming soon" when the date is unknown', () => {
|
||||||
|
render(<ComingBadge game={{coming: true, comingOn: null}}/>);
|
||||||
|
|
||||||
|
expect(screen.getByText('LIBRARY.COMING_SOON')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows nothing for a released game', () => {
|
||||||
|
const {container} = render(<ComingBadge game={{coming: false, comingOn: null}}/>);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows nothing when an older Core sends no field', () => {
|
||||||
|
const {container} = render(<ComingBadge game={{appid: 220}}/>);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import {Card} from '@/components/ui/card';
|
|||||||
import {cn} from '@/lib/utils';
|
import {cn} from '@/lib/utils';
|
||||||
import GameCover from './GameCover.jsx';
|
import GameCover from './GameCover.jsx';
|
||||||
import FitText from './FitText.jsx';
|
import FitText from './FitText.jsx';
|
||||||
|
import ComingBadge from './ComingBadge.jsx';
|
||||||
import GameStatusBadge from './GameStatusBadge.jsx';
|
import GameStatusBadge from './GameStatusBadge.jsx';
|
||||||
import ProfileArtMenu from './ProfileArtMenu.jsx';
|
import ProfileArtMenu from './ProfileArtMenu.jsx';
|
||||||
import TimeToBeat from './TimeToBeat.jsx';
|
import TimeToBeat from './TimeToBeat.jsx';
|
||||||
@@ -77,6 +78,7 @@ export default function GameHeroCard({game, auditor, profileStyle, onCorrect, on
|
|||||||
<Clock className="size-3"/>
|
<Clock className="size-3"/>
|
||||||
{t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.playtimeMinutes)})}
|
{t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.playtimeMinutes)})}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<ComingBadge game={game}/>
|
||||||
<GameStatusBadge status={game.status}/>
|
<GameStatusBadge status={game.status}/>
|
||||||
{platformLabel(game) && (
|
{platformLabel(game) && (
|
||||||
<Badge variant="outline"><Gamepad2 className="size-3"/>{platformLabel(game)}</Badge>
|
<Badge variant="outline"><Gamepad2 className="size-3"/>{platformLabel(game)}</Badge>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {cn} from '@/lib/utils';
|
|||||||
import GameCover from './GameCover.jsx';
|
import GameCover from './GameCover.jsx';
|
||||||
import PileTrend from './stats/PileTrend.jsx';
|
import PileTrend from './stats/PileTrend.jsx';
|
||||||
import {StatCount} from './stats/StatDrill.jsx';
|
import {StatCount} from './stats/StatDrill.jsx';
|
||||||
import {backlogShare} from '../utils/backlog.js';
|
import {backlogShare, pileGames} from '../utils/backlog.js';
|
||||||
import {sliceColors, statusSlices} from '../utils/statCharts.js';
|
import {sliceColors, statusSlices} from '../utils/statCharts.js';
|
||||||
import {formatPlaytime} from '../utils/games.js';
|
import {formatPlaytime} from '../utils/games.js';
|
||||||
import {libraryLink} from '../utils/statGames.js';
|
import {libraryLink} from '../utils/statGames.js';
|
||||||
@@ -18,7 +18,7 @@ import {libraryLink} from '../utils/statGames.js';
|
|||||||
*/
|
*/
|
||||||
export default function PileOfShameCard({summary, trend, syncing, onShuffle, shuffling}) {
|
export default function PileOfShameCard({summary, trend, syncing, onShuffle, shuffling}) {
|
||||||
const {t, i18n} = useTranslation();
|
const {t, i18n} = useTranslation();
|
||||||
const backlog = summary.byStatus?.BACKLOG ?? 0;
|
const backlog = pileGames(summary);
|
||||||
const suggestion = summary.suggestion;
|
const suggestion = summary.suggestion;
|
||||||
const slices = statusSlices(summary.byStatus);
|
const slices = statusSlices(summary.byStatus);
|
||||||
const colors = sliceColors(slices);
|
const colors = sliceColors(slices);
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ describe('PileOfShameCard', () => {
|
|||||||
expect(screen.queryByText('DASHBOARD.BACKLOG_EMPTY')).toBeNull();
|
expect(screen.queryByText('DASHBOARD.BACKLOG_EMPTY')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts the pile without the games that are not released yet', () => {
|
||||||
|
renderCard({summary: summary(5, {pileGames: 3})});
|
||||||
|
|
||||||
|
expect(screen.getByText('3', {selector: '.text-4xl'})).toBeTruthy();
|
||||||
|
expect(screen.queryByText('5', {selector: '.text-4xl'})).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the BACKLOG status count when an older Core sends no pileGames', () => {
|
||||||
|
renderCard({summary: summary(5)});
|
||||||
|
|
||||||
|
expect(screen.getByText('5', {selector: '.text-4xl'})).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('hides the shuffle button when there is nothing else to pick', () => {
|
it('hides the shuffle button when there is nothing else to pick', () => {
|
||||||
renderCard({summary: summary(1)});
|
renderCard({summary: summary(1)});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {Button} from '@/components/ui/button';
|
|||||||
import {Dialog, DialogContent, DialogDescription, DialogTitle} from '@/components/ui/dialog';
|
import {Dialog, DialogContent, DialogDescription, DialogTitle} from '@/components/ui/dialog';
|
||||||
import {Skeleton} from '@/components/ui/skeleton';
|
import {Skeleton} from '@/components/ui/skeleton';
|
||||||
import GameRow from './GameRow.jsx';
|
import GameRow from './GameRow.jsx';
|
||||||
|
import ComingBadge from '../ComingBadge.jsx';
|
||||||
import GameStatusBadge from '../GameStatusBadge.jsx';
|
import GameStatusBadge from '../GameStatusBadge.jsx';
|
||||||
import {formatDate} from '../../utils/format.js';
|
import {formatDate} from '../../utils/format.js';
|
||||||
import {formatPlaytime} from '../../utils/games.js';
|
import {formatPlaytime} from '../../utils/games.js';
|
||||||
@@ -52,6 +53,7 @@ export default function StatGamesDialog({request, onClose, onCloseAutoFocus}) {
|
|||||||
const moved = game.fromStatus || game.toStatus;
|
const moved = game.fromStatus || game.toStatus;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ComingBadge game={game} compact/>
|
||||||
{!moved && game.status && <GameStatusBadge status={game.status} compact/>}
|
{!moved && game.status && <GameStatusBadge status={game.status} compact/>}
|
||||||
{moved && (
|
{moved && (
|
||||||
<span>{game.fromStatus ? t(`BACKLOG.STATUS.${game.fromStatus}`) : t('STATS.DRILL.NEW_IN_LIBRARY')}
|
<span>{game.fromStatus ? t(`BACKLOG.STATUS.${game.fromStatus}`) : t('STATS.DRILL.NEW_IN_LIBRARY')}
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ describe('StatGamesDialog', () => {
|
|||||||
expect(mock.mock.calls[0][1].headers['X-Profile-Ticket']).toBe('old');
|
expect(mock.mock.calls[0][1].headers['X-Profile-Ticket']).toBe('old');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('showsTheComingBadgeOnlyForAGameNotReleasedYet', async () => {
|
||||||
|
fetchRoutes({[`GET ${GAMES}`]: page([
|
||||||
|
{appid: 5, name: 'Dawn of War 4', status: 'BACKLOG', playtimeMinutes: 0, coming: true, comingOn: '2026-12-04'},
|
||||||
|
{appid: 6, name: 'Portal 2', status: 'BACKLOG', playtimeMinutes: 0, coming: false, comingOn: null}], 0, 1, 2)});
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await screen.findByText('Portal 2');
|
||||||
|
|
||||||
|
expect(screen.getAllByText('LIBRARY.COMING')).toHaveLength(1);
|
||||||
|
expect(screen.getByText('Dawn of War 4').closest('li').textContent).toContain('LIBRARY.COMING');
|
||||||
|
});
|
||||||
|
|
||||||
it('showsTheStatusMoveOfATrendRow', async () => {
|
it('showsTheStatusMoveOfATrendRow', async () => {
|
||||||
fetchRoutes({[`GET ${GAMES}`]: page(
|
fetchRoutes({[`GET ${GAMES}`]: page(
|
||||||
[{appid: 5, name: 'Moved', fromStatus: 'BACKLOG', toStatus: 'PLAYING', at: '2026-09-20T10:00:00Z'}], 0, 1, 1)});
|
[{appid: 5, name: 'Moved', fromStatus: 'BACKLOG', toStatus: 'PLAYING', at: '2026-09-20T10:00:00Z'}], 0, 1, 1)});
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GameDetail', () => {
|
describe('GameDetail', () => {
|
||||||
|
it('shows the coming badge of a game that is not released yet', async () => {
|
||||||
|
answer({...game, source: 'STEAM', coming: true, comingOn: '2026-12-04'});
|
||||||
|
renderDetail();
|
||||||
|
|
||||||
|
expect(await screen.findByText('LIBRARY.COMING')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows no coming badge for a released game', async () => {
|
||||||
|
answer({...game, source: 'STEAM', coming: false, comingOn: null});
|
||||||
|
renderDetail();
|
||||||
|
|
||||||
|
await screen.findByText('Half-Life 2');
|
||||||
|
|
||||||
|
expect(screen.queryByText('LIBRARY.COMING')).toBeNull();
|
||||||
|
expect(screen.queryByText('LIBRARY.COMING_SOON')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('asks "finished?" instead of "playing?" when Core has a finished hint', async () => {
|
it('asks "finished?" instead of "playing?" when Core has a finished hint', async () => {
|
||||||
answer({...game, playtime2Weeks: 90, source: 'STEAM', finishedHint: {type: 'ENDING', achievement: 'Survivor'}});
|
answer({...game, playtime2Weeks: 90, source: 'STEAM', finishedHint: {type: 'ENDING', achievement: 'Survivor'}});
|
||||||
renderDetail();
|
renderDetail();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import AddGamePanel from '../components/AddGamePanel.jsx';
|
|||||||
import GameCover from '../components/GameCover.jsx';
|
import GameCover from '../components/GameCover.jsx';
|
||||||
import FitText from '../components/FitText.jsx';
|
import FitText from '../components/FitText.jsx';
|
||||||
import LibraryTabs from '../components/LibraryTabs.jsx';
|
import LibraryTabs from '../components/LibraryTabs.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 {usePreferences} from '../context/PreferencesContext.jsx';
|
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||||
@@ -316,6 +317,7 @@ export default function GameLibrary() {
|
|||||||
<Layers className="size-3"/>x{g.editions}
|
<Layers className="size-3"/>x{g.editions}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
<ComingBadge game={g} compact/>
|
||||||
<GameStatusBadge status={g.status} compact/>
|
<GameStatusBadge status={g.status} compact/>
|
||||||
{g.achievementsTotal > 0 && g.achievementsUnlocked != null && (
|
{g.achievementsTotal > 0 && g.achievementsUnlocked != null && (
|
||||||
<Badge variant="secondary" className="hidden sm:inline-flex tabular-nums"
|
<Badge variant="secondary" className="hidden sm:inline-flex tabular-nums"
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GameLibrary search', () => {
|
describe('GameLibrary search', () => {
|
||||||
|
it('shows the coming badge only for a game that is not released yet', async () => {
|
||||||
|
answer([
|
||||||
|
{...page.content[0], coming: true, comingOn: '2026-12-04'},
|
||||||
|
{appid: 620, name: 'Portal 2', playtimeMinutes: 0, status: 'BACKLOG', coming: false, comingOn: null},
|
||||||
|
]);
|
||||||
|
renderLibrary();
|
||||||
|
|
||||||
|
await screen.findByText('Portal 2');
|
||||||
|
|
||||||
|
expect(screen.getAllByText('LIBRARY.COMING')).toHaveLength(1);
|
||||||
|
expect(screen.getByText('Half-Life 2').closest('li').textContent).toContain('LIBRARY.COMING');
|
||||||
|
expect(screen.getByText('Portal 2').closest('li').textContent).not.toContain('LIBRARY.COMING');
|
||||||
|
});
|
||||||
|
|
||||||
it('shows the achievement progress of a game when it is known', async () => {
|
it('shows the achievement progress of a game when it is known', async () => {
|
||||||
answer([
|
answer([
|
||||||
{...page.content[0], achievementsUnlocked: 3, achievementsTotal: 4},
|
{...page.content[0], achievementsUnlocked: 3, achievementsTotal: 4},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {Skeleton} from '@/components/ui/skeleton';
|
|||||||
import {cn} from '@/lib/utils';
|
import {cn} from '@/lib/utils';
|
||||||
import GameCover from '../components/GameCover.jsx';
|
import GameCover from '../components/GameCover.jsx';
|
||||||
import FitText from '../components/FitText.jsx';
|
import FitText from '../components/FitText.jsx';
|
||||||
|
import ComingBadge from '../components/ComingBadge.jsx';
|
||||||
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
||||||
import {formatDay} from '../utils/format.js';
|
import {formatDay} from '../utils/format.js';
|
||||||
import {backgroundEffect, bannerEffect, coverEffect, equippedAccent, equippedSkin, lookClass} from '../utils/rewards.js';
|
import {backgroundEffect, bannerEffect, coverEffect, equippedAccent, equippedSkin, lookClass} from '../utils/rewards.js';
|
||||||
@@ -242,6 +243,7 @@ function Playing({games, compact}) {
|
|||||||
<GameCover appid={g.appid} imageUrl={g.imageUrl} className="w-full"/>
|
<GameCover appid={g.appid} imageUrl={g.imageUrl} className="w-full"/>
|
||||||
<div className="@container text-sm font-medium"><FitText text={g.name ?? `App ${g.appid}`}/></div>
|
<div className="@container text-sm font-medium"><FitText text={g.name ?? `App ${g.appid}`}/></div>
|
||||||
<div className="text-xs text-muted-foreground tabular-nums">{formatPlaytime(g.playtimeMinutes)}</div>
|
<div className="text-xs text-muted-foreground tabular-nums">{formatPlaytime(g.playtimeMinutes)}</div>
|
||||||
|
<ComingBadge game={g}/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -269,6 +271,7 @@ function Finished({games, equipped}) {
|
|||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<ComingBadge game={g} compact/>
|
||||||
<GameStatusBadge status={g.status} compact/>
|
<GameStatusBadge status={g.status} compact/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ const renderPage = (name = 'Lobo') => renderWithProviders(<PublicProfilePage/>,
|
|||||||
describe('PublicProfilePage', () => {
|
describe('PublicProfilePage', () => {
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
it('shows the coming badge on a game that is not released yet, in both lists', async () => {
|
||||||
|
mockBackends({answer: {...library,
|
||||||
|
playing: [{...library.playing[0], status: 'PLAYING', coming: true, comingOn: '2026-12-04'}],
|
||||||
|
finished: [{...library.finished[0], coming: false, comingOn: null}]}});
|
||||||
|
renderPage();
|
||||||
|
|
||||||
|
await screen.findByText('Portal 2');
|
||||||
|
|
||||||
|
expect(screen.getAllByText('LIBRARY.COMING')).toHaveLength(1);
|
||||||
|
expect(screen.getByText('Half-Life 2').closest('li').textContent).toContain('LIBRARY.COMING');
|
||||||
|
expect(screen.getByText('Portal 2').closest('li').textContent).not.toContain('LIBRARY.COMING');
|
||||||
|
});
|
||||||
|
|
||||||
it('wears the premium accent and the badge of the owner', async () => {
|
it('wears the premium accent and the badge of the owner', async () => {
|
||||||
mockBackends({style: {accent: 'red'}, equipped: {ACCENT: 'ACCENT_GOLD', BADGE: 'BADGE_PILE_SLAYER', BANNER_FRAME: 'FRAME_GOLD'}});
|
mockBackends({style: {accent: 'red'}, equipped: {ACCENT: 'ACCENT_GOLD', BADGE: 'BADGE_PILE_SLAYER', BANNER_FRAME: 'FRAME_GOLD'}});
|
||||||
renderPage();
|
renderPage();
|
||||||
|
|||||||
@@ -66,5 +66,8 @@ export const libraryQuery = ({page, size, sort, status, q, maxMinutes, hidden, h
|
|||||||
return params.toString();
|
return params.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// pileGames is missing from an older Core
|
||||||
|
export const pileGames = (summary) => summary?.pileGames ?? summary?.byStatus?.BACKLOG ?? 0;
|
||||||
|
|
||||||
export const backlogShare = (summary) =>
|
export const backlogShare = (summary) =>
|
||||||
summary?.total ? Math.round(((summary.byStatus?.BACKLOG ?? 0) / summary.total) * 100) : 0;
|
summary?.total ? Math.round((pileGames(summary) / summary.total) * 100) : 0;
|
||||||
@@ -43,6 +43,10 @@ describe('backlogShare', () => {
|
|||||||
expect(backlogShare({total: 3, byStatus: {BACKLOG: 2}})).toBe(67);
|
expect(backlogShare({total: 3, byStatus: {BACKLOG: 2}})).toBe(67);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the pile without coming games when Core sends it', () => {
|
||||||
|
expect(backlogShare({total: 4, byStatus: {BACKLOG: 3}, pileGames: 1})).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
it('is 0 for an empty library', () => {
|
it('is 0 for an empty library', () => {
|
||||||
expect(backlogShare({total: 0, byStatus: {BACKLOG: 0}})).toBe(0);
|
expect(backlogShare({total: 0, byStatus: {BACKLOG: 0}})).toBe(0);
|
||||||
expect(backlogShare(null)).toBe(0);
|
expect(backlogShare(null)).toBe(0);
|
||||||
|
|||||||
Reference in new issue
Block a user