diff --git a/public/i18n/de.json b/public/i18n/de.json
index 8880dee..899be1b 100644
--- a/public/i18n/de.json
+++ b/public/i18n/de.json
@@ -575,7 +575,6 @@
"TREND_ADDED": "Diesen Monat zum Stapel hinzugekommen",
"TREND_DONE": "Diesen Monat vom Stapel gegangen",
"NEW_IN_LIBRARY": "Neu",
- "OPEN": "Im Steam-Shop öffnen",
"GAMES_one": "{{count}} Spiel",
"GAMES_other": "{{count}} Spiele"
}
diff --git a/public/i18n/en.json b/public/i18n/en.json
index d9c0d99..634686a 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -575,7 +575,6 @@
"TREND_ADDED": "Added to the pile this month",
"TREND_DONE": "Off the pile this month",
"NEW_IN_LIBRARY": "New",
- "OPEN": "Open in the Steam store",
"GAMES_one": "{{count}} game",
"GAMES_other": "{{count}} games"
}
diff --git a/src/components/AchievementsCard.jsx b/src/components/AchievementsCard.jsx
index b7a66c2..f7d6014 100644
--- a/src/components/AchievementsCard.jsx
+++ b/src/components/AchievementsCard.jsx
@@ -5,6 +5,7 @@ import {Trophy} from 'lucide-react';
import {Card} from '@/components/ui/card';
import GameCover from './GameCover.jsx';
import FitText from './FitText.jsx';
+import {StatCount} from './stats/StatDrill.jsx';
/**
* @param summary the answer of GET /api/library/achievements/summary
@@ -15,12 +16,14 @@ export default function AchievementsCard({summary}) {
const started = summary.startedGames ?? 0;
const pending = summary.pendingGames ?? 0;
const hidden = summary.hiddenGames ?? 0;
+ const perfect = summary.perfectGames ?? 0;
// A private profile keeps games pending; the privacy hint explains that better than the pending count.
const steamHidesAll = hidden > 0 && started === 0;
const stats = [
[(summary.unlocked ?? 0).toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.UNLOCKED')],
- [(summary.perfectGames ?? 0).toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.PERFECT')],
+ [perfect.toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.PERFECT'),
+ {params: {key: 'PERFECT'}, count: perfect}],
[summary.averageCompletion != null ? `${summary.averageCompletion.toLocaleString(i18n.language)} %` : '–',
t('DASHBOARD.ACHIEVEMENTS.AVERAGE')],
];
@@ -33,9 +36,11 @@ export default function AchievementsCard({summary}) {
- {stats.map(([value, label]) => (
+ {stats.map(([value, label, drill]) => (
-
{value}
+
+ {value}
+
{label}
))}
diff --git a/src/components/AchievementsCard.test.jsx b/src/components/AchievementsCard.test.jsx
index 419e18d..d8cfac7 100644
--- a/src/components/AchievementsCard.test.jsx
+++ b/src/components/AchievementsCard.test.jsx
@@ -1,8 +1,10 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
-import {cleanup, render, screen} from '@testing-library/react';
+import {cleanup, fireEvent, render, screen} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import AchievementsCard from './AchievementsCard.jsx';
+import {StatDrillProvider} from './stats/StatDrill.jsx';
+import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
@@ -49,4 +51,22 @@ describe('AchievementsCard', () => {
expect(screen.getByText('DASHBOARD.ACHIEVEMENTS.NONE')).toBeTruthy();
});
+
+ it('opensThePerfectGamesInTheOwnLibrary', async () => {
+ fetchRoutes({'GET /api/library/stats/games': json(200, {
+ content: [{appid: -7, name: 'Manual Game'}], page: 0, size: 40, totalElements: 1, totalPages: 1})});
+ render(
);
+
+ fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.ACHIEVEMENTS.PERFECT: 2'}));
+
+ const link = await screen.findByRole('link', {name: /Manual Game/});
+ expect(fetch.mock.calls[0][0]).toContain('key=PERFECT&page=0');
+ expect(link.getAttribute('href')).toBe('/library/-7');
+ });
+
+ it('perfectGamesAreNotClickableAtZero', () => {
+ render(
);
+
+ expect(screen.queryByRole('button')).toBeNull();
+ });
});
diff --git a/src/components/PileOfShameCard.jsx b/src/components/PileOfShameCard.jsx
index 3856fa1..b7bebe6 100644
--- a/src/components/PileOfShameCard.jsx
+++ b/src/components/PileOfShameCard.jsx
@@ -36,7 +36,7 @@ export default function PileOfShameCard({summary, trend, syncing, onShuffle, shu
-
{backlog.toLocaleString(i18n.language)}
@@ -77,7 +77,8 @@ export default function PileOfShameCard({summary, trend, syncing, onShuffle, shu
{t(`BACKLOG.STATUS.${s}`)}
-
+
{summary.byStatus[s]}
diff --git a/src/components/PileOfShameCard.test.jsx b/src/components/PileOfShameCard.test.jsx
index 0491577..ffb5850 100644
--- a/src/components/PileOfShameCard.test.jsx
+++ b/src/components/PileOfShameCard.test.jsx
@@ -65,6 +65,16 @@ describe('PileOfShameCard', () => {
expect(screen.getByText('DASHBOARD.BACKLOG_EMPTY')).toBeTruthy();
});
+ it('zeroCountsAreNotClickable', () => {
+ render(
+ {}} summary={summary(0)}/>
+ );
+
+ expect(screen.queryByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 0'})).toBeNull();
+ expect(screen.queryByRole('link', {name: 'BACKLOG.STATUS.FINISHED: 0'})).toBeNull();
+ expect(screen.getByRole('link', {name: 'BACKLOG.STATUS.PLAYING: 3'})).toBeTruthy();
+ });
+
it('shows the backlog hours when IGDB knows some', () => {
renderCard({summary: summary(2, {backlogMinutes: 7440})});
diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx
index 937025d..85bef4f 100644
--- a/src/components/StatCard.jsx
+++ b/src/components/StatCard.jsx
@@ -32,7 +32,7 @@ function useCountUp(target, durationMs = 1600) {
export default function StatCard({label, value, icon: Icon, suffix, drill}) {
const {i18n} = useTranslation();
const animated = useCountUp(value);
- const target = useDrillTarget({title: label, ...drill});
+ const target = useDrillTarget({title: label, count: value, ...drill});
const card = (
{Icon && (
diff --git a/src/components/SteamGameCount.jsx b/src/components/SteamGameCount.jsx
index 6229c3b..5e1f7bb 100644
--- a/src/components/SteamGameCount.jsx
+++ b/src/components/SteamGameCount.jsx
@@ -6,6 +6,7 @@ import {ArrowRight, Gamepad2} from 'lucide-react';
import {Card} from '@/components/ui/card';
const DURATION_MS = 1000;
+const COUNT_CLASS = 'block text-5xl font-bold tracking-tight tabular-nums';
const SteamGameCount = ({targetCount}) => {
const {t} = useTranslation();
@@ -33,7 +34,9 @@ const SteamGameCount = ({targetCount}) => {
-
{count}
+ {targetCount > 0
+ ?
{count}
+ :
{count} }
{t('DASHBOARD.STEAM_GAMES')}
diff --git a/src/components/SteamGameCount.test.jsx b/src/components/SteamGameCount.test.jsx
new file mode 100644
index 0000000..d082c2a
--- /dev/null
+++ b/src/components/SteamGameCount.test.jsx
@@ -0,0 +1,20 @@
+// @vitest-environment jsdom
+import {afterEach, describe, expect, it, vi} from 'vitest';
+import {cleanup, render, screen} from '@testing-library/react';
+import {MemoryRouter} from 'react-router-dom';
+import SteamGameCount from './SteamGameCount.jsx';
+
+vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
+
+describe('SteamGameCount', () => {
+ afterEach(cleanup);
+
+ it('linksTheCountToTheLibraryOnlyWhenThereAreGames', () => {
+ const {container, rerender} = render(
);
+ expect(container.querySelectorAll('a[href="/library"]')).toHaveLength(1);
+
+ rerender(
);
+ expect(container.querySelectorAll('a[href="/library"]')).toHaveLength(2);
+ expect(screen.getByText('DASHBOARD.STEAM_GAMES')).toBeTruthy();
+ });
+});
diff --git a/src/components/stats/GameRow.jsx b/src/components/stats/GameRow.jsx
index a6b0246..5fc8d31 100644
--- a/src/components/stats/GameRow.jsx
+++ b/src/components/stats/GameRow.jsx
@@ -1,15 +1,13 @@
import {Link} from 'react-router-dom';
-import {useTranslation} from 'react-i18next';
import GameCover from '../GameCover.jsx';
import FitText from '../FitText.jsx';
import {gameHref} from '../../utils/statGames.js';
/**
- * A game with its cover and a value on the right; links to the game page in the user's own library, to the Steam
- * store for a visitor.
+ * A game with its cover and a value on the right; links to the game page when the games are the viewer's own, plain
+ * otherwise.
*/
export default function GameRow({game, value, linked = true}) {
- const {t} = useTranslation();
const content = (
<>
@@ -17,14 +15,11 @@ export default function GameRow({game, value, linked = true}) {
{value}
>
);
- const className = 'flex items-center gap-3 hover:text-primary transition-colors';
- if (linked) return
{content};
- const href = gameHref(game.appid, false);
- if (href) {
+ if (linked) {
return (
-
+
{content}
-
+
);
}
return
{content}
;
diff --git a/src/components/stats/HabitsStats.jsx b/src/components/stats/HabitsStats.jsx
index 2ab4d74..527be0d 100644
--- a/src/components/stats/HabitsStats.jsx
+++ b/src/components/stats/HabitsStats.jsx
@@ -25,7 +25,7 @@ export default function HabitsStats({habits}) {
+ drill={{params: {key: 'UNDER_ONE_HOUR'}, count: habits.underOneHourGames}}/>
)}
diff --git a/src/components/stats/OverviewStats.jsx b/src/components/stats/OverviewStats.jsx
index cebf363..a9b144f 100644
--- a/src/components/stats/OverviewStats.jsx
+++ b/src/components/stats/OverviewStats.jsx
@@ -15,18 +15,19 @@ export default function OverviewStats({overview}) {
+ drill={{to: libraryLink(), params: {key: 'TOTAL'}, count: overview.totalGames}}/>
+ drill={{params: {key: 'PLAYED'}, count: overview.playedGames}}/>
+ drill={{params: {key: 'NEVER_STARTED'}, count: overview.neverStarted}}/>
{GAME_STATUSES.map((s) => (
{t(`BACKLOG.STATUS.${s}`)}{' '}
{n(overview.byStatus[s] ?? 0)}
diff --git a/src/components/stats/PileStats.jsx b/src/components/stats/PileStats.jsx
index b60df7b..e5d1866 100644
--- a/src/components/stats/PileStats.jsx
+++ b/src/components/stats/PileStats.jsx
@@ -18,10 +18,10 @@ export default function PileStats({pile, linked = true}) {
+ drill={{to: libraryLink('BACKLOG'), params: {key: 'PILE'}, count: pile.backlogGames}}/>
+ drill={{params: {key: 'PILE_TIME_TO_BEAT'}, count: pile.gamesWithTimeToBeat}}/>
{pile.normalMinutes > 0 && (
diff --git a/src/components/stats/StatDrill.jsx b/src/components/stats/StatDrill.jsx
index 21842b7..04fdc52 100644
--- a/src/components/stats/StatDrill.jsx
+++ b/src/components/stats/StatDrill.jsx
@@ -15,8 +15,9 @@ const DRILL_CLASS = 'inline-flex items-center justify-center max-sm:min-h-11 max
*
* @param source own, profile, community or points
* @param ticket profile: the ticket for the visitor's requests
+ * @param linked the games are the viewer's own: rows link to the detail page
*/
-export function StatDrillProvider({source, ticket, children}) {
+export function StatDrillProvider({source, ticket, linked = false, children}) {
const [request, setRequest] = useState(null);
const opener = useRef(null);
useEffect(() => {
@@ -26,9 +27,9 @@ export function StatDrillProvider({source, ticket, children}) {
source,
open: (title, params) => {
opener.current = document.activeElement;
- setRequest({title, source, ticket, params});
+ setRequest({title, source, ticket, linked, params});
},
- }), [source, ticket]);
+ }), [source, ticket, linked]);
return (
{children}
@@ -43,9 +44,11 @@ export const useStatDrill = () => useContext(StatDrillContext);
/**
* What a number does on click: a library link (own library only, `to`), the dialog (`params`), or nothing (null).
+ * Nothing at `count` 0; `count` undefined means no count is known.
*/
-export function useDrillTarget({to, params, title}) {
+export function useDrillTarget({to, params, title, count}) {
const drill = useStatDrill();
+ if (count === 0) return null;
if (to && drill?.source === 'own') return {to};
if (drill && params) return {onClick: () => drill.open(title, params)};
return null;
@@ -80,9 +83,10 @@ export function DrillButton({onClick, label, className, children}) {
*
* @param to library URL, used instead of the dialog in the user's own views
* @param params `{key, arg}` or `{track}` / `{kind, ref}` of the games request
+ * @param count the number shown; 0 is never clickable
*/
-export function StatCount({to, params, title, label, className, children}) {
- const target = useDrillTarget({to, params, title});
+export function StatCount({to, params, title, label, count, className, children}) {
+ const target = useDrillTarget({to, params, title, count});
if (!target) return children;
return target.to
? {children}
diff --git a/src/components/stats/StatDrill.test.jsx b/src/components/stats/StatDrill.test.jsx
new file mode 100644
index 0000000..3704e37
--- /dev/null
+++ b/src/components/stats/StatDrill.test.jsx
@@ -0,0 +1,25 @@
+// @vitest-environment jsdom
+import {afterEach, describe, expect, it, vi} from 'vitest';
+import {cleanup, render, screen} from '@testing-library/react';
+import {MemoryRouter} from 'react-router-dom';
+import {StatCount, StatDrillProvider} from './StatDrill.jsx';
+
+vi.mock('react-i18next', () => import('../../test/i18n-mock.js'));
+
+const renderCount = (count) => render(
+
+ {count}
+ );
+
+describe('StatCount', () => {
+ afterEach(cleanup);
+
+ it('aZeroCountIsNotClickable', () => {
+ renderCount(0);
+ expect(screen.queryByRole('button')).toBeNull();
+
+ cleanup();
+ renderCount(3);
+ expect(screen.getByRole('button')).toBeTruthy();
+ });
+});
diff --git a/src/components/stats/StatGamesDialog.jsx b/src/components/stats/StatGamesDialog.jsx
index 21588c6..dbd3971 100644
--- a/src/components/stats/StatGamesDialog.jsx
+++ b/src/components/stats/StatGamesDialog.jsx
@@ -15,7 +15,7 @@ const ERRORS = {403: 'STATS.DRILL.NOT_SHARED', 401: 'STATS.DRILL.EXPIRED'};
/**
* The games behind one number, 40 per page.
*
- * @param request `{title, source, ticket, params}` or null (closed)
+ * @param request `{title, source, ticket, linked, params}` or null (closed)
*/
export default function StatGamesDialog({request, onClose, onCloseAutoFocus}) {
const {t, i18n} = useTranslation();
@@ -75,7 +75,7 @@ export default function StatGamesDialog({request, onClose, onCloseAutoFocus}) {
{state.games.map((game, index) => (
-
+
))}
diff --git a/src/components/stats/StatGamesDialog.test.jsx b/src/components/stats/StatGamesDialog.test.jsx
index 88d1aa2..638181d 100644
--- a/src/components/stats/StatGamesDialog.test.jsx
+++ b/src/components/stats/StatGamesDialog.test.jsx
@@ -128,4 +128,31 @@ describe('StatGamesDialog', () => {
expect(screen.queryByText('Old game')).toBeNull();
expect(screen.getByText('New game')).toBeTruthy();
});
+
+ it('aProfileRowIsPlainForAVisitor', async () => {
+ fetchRoutes({'GET /api/public/profiles/library/stats/games': page(rows(1, 1), 0, 1, 1)});
+ renderDialog({source: 'profile', ticket: 't'});
+
+ await screen.findByText('Game 1');
+
+ expect(screen.queryByRole('link')).toBeNull();
+ });
+
+ it('aProfileRowOpensTheDetailPageForTheOwner', async () => {
+ fetchRoutes({'GET /api/public/profiles/library/stats/games': page(rows(1, 1), 0, 1, 1)});
+ renderDialog({source: 'profile', ticket: 't', linked: true});
+
+ const link = await screen.findByRole('link', {name: /Game 1/});
+
+ expect(link.getAttribute('href')).toBe('/library/1');
+ });
+
+ it('aCommunityRowIsPlain', async () => {
+ fetchRoutes({'GET /api/stats/public/games': page(rows(1, 1, {libraries: 2, minutes: 60}), 0, 1, 1)});
+ renderDialog({source: 'community'});
+
+ await screen.findByText('Game 1');
+
+ expect(screen.queryByRole('link')).toBeNull();
+ });
});
diff --git a/src/components/stats/statGroupTile.jsx b/src/components/stats/statGroupTile.jsx
index 02f857f..fb011b4 100644
--- a/src/components/stats/statGroupTile.jsx
+++ b/src/components/stats/statGroupTile.jsx
@@ -12,7 +12,7 @@ import PlatformStats from './PlatformStats.jsx';
* null tile, and an element is never null.
*
* @param stats the answer of GET /api/library/stats, or the stats of a public profile
- * @param linked the game rows link to the game pages; false on the public profile
+ * @param linked true for the user's own games: the dashboard and the owner on the profile
*/
export default function statGroupTile(id, stats, linked = true) {
switch (id) {
diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx
index 2af4486..9ca85f4 100644
--- a/src/pages/LandingPage.jsx
+++ b/src/pages/LandingPage.jsx
@@ -17,7 +17,6 @@ import {TagBars} from '../components/stats/TasteStats.jsx';
import {StatDrillProvider, useStatDrill, useTagDrill} from '../components/stats/StatDrill.jsx';
import useSession from '../hooks/useSession.js';
import {genreKey} from '../utils/stats.js';
-import {gameHref} from '../utils/statGames.js';
import usePublicStats from '../hooks/usePublicStats.js';
@@ -51,11 +50,9 @@ function RankedGames({title, games, formatValue, drillKey}) {
}
function GameName({game}) {
- const href = gameHref(game.appid, false);
- const name = ;
return (
- {href ? {name} : name}
+
);
}
diff --git a/src/pages/LandingPage.test.jsx b/src/pages/LandingPage.test.jsx
index 55a926e..482e532 100644
--- a/src/pages/LandingPage.test.jsx
+++ b/src/pages/LandingPage.test.jsx
@@ -43,6 +43,15 @@ describe('LandingPage', () => {
}
});
+ it('topGamesHaveNoStoreLink', async () => {
+ answer({topPlayedGames: [{appid: 220, name: 'Half-Life 2', value: 600}]});
+ const {container} = renderPage();
+
+ await screen.findByText('Half-Life 2');
+
+ expect(container.querySelector('a[href*="steampowered"]')).toBeNull();
+ });
+
it('opensTheGamesBehindTheTotalButNotBehindOneUsersLibrary', async () => {
fetchRoutes({
'GET /api/stats/public': json(200, {topPlayedGames: [], topOwnedGames: [], totalLibraryEntries: 9, biggestLibrary: 4}),
@@ -58,4 +67,13 @@ describe('LandingPage', () => {
expect(screen.queryByRole('button', {name: /LANDING\.BIGGEST_LIBRARY/})).toBeNull();
expect(screen.queryByRole('button', {name: /LANDING\.TOTAL_USERS/})).toBeNull();
});
+
+ it('aZeroTotalIsNotClickable', async () => {
+ answer({totalLibraryEntries: 0});
+ renderPage();
+
+ await screen.findByText('LANDING.TOTAL_GAMES');
+
+ expect(screen.queryByRole('button', {name: /LANDING\.TOTAL_GAMES/})).toBeNull();
+ });
});
diff --git a/src/pages/PointsPage.jsx b/src/pages/PointsPage.jsx
index a7fb069..961e718 100644
--- a/src/pages/PointsPage.jsx
+++ b/src/pages/PointsPage.jsx
@@ -203,7 +203,7 @@ function LockedProgress({item, progress}) {
const text = t(`POINTS.PROGRESS.${item.track}`, {count, target: item.target});
// Playtime months compare two numbers: no list of games shows them.
const target = useDrillTarget({
- params: item.track === 'PLAY_MORE_MONTHS' ? null : {track: item.track}, title: t(`POINTS.ITEMS.${item.code}`),
+ params: item.track === 'PLAY_MORE_MONTHS' ? null : {track: item.track}, title: t(`POINTS.ITEMS.${item.code}`), count,
});
const className = 'flex w-40 shrink-0 flex-col gap-1 text-xs text-muted-foreground';
const Wrapper = target ? 'button' : 'div';
diff --git a/src/pages/PointsPage.test.jsx b/src/pages/PointsPage.test.jsx
index a4d152c..0ae372e 100644
--- a/src/pages/PointsPage.test.jsx
+++ b/src/pages/PointsPage.test.jsx
@@ -121,6 +121,13 @@ describe('PointsPage', () => {
expect(within(item).queryByRole('button')).toBeNull();
});
+ it('shows no button on a locked bar without progress', async () => {
+ await renderPage({'GET /api/rewards/wallet': json(200, wallet({progress: {GAMES_FINISHED: 0}}))});
+
+ const item = screen.getByText('POINTS.ITEMS.BADGE_FINISH_10').closest('li');
+ expect(within(item).queryByRole('button')).toBeNull();
+ });
+
it('caps the progress at the target', async () => {
await renderPage();
diff --git a/src/pages/PublicProfilePage.jsx b/src/pages/PublicProfilePage.jsx
index 9267167..293f82b 100644
--- a/src/pages/PublicProfilePage.jsx
+++ b/src/pages/PublicProfilePage.jsx
@@ -138,7 +138,7 @@ export default function PublicProfilePage() {
&& ;
case 'FINISHED':
return library.finished?.length > 0 && ;
- default: return statGroupTile(id, library.stats, false);
+ default: return statGroupTile(id, library.stats, owner);
}
};
// Core sends only the data of the shown tiles (the ticket says which), so a new tile needs a new load.
@@ -178,7 +178,7 @@ export default function PublicProfilePage() {
)}
-
+
setEditing(false)}/>
diff --git a/src/pages/PublicProfilePage.test.jsx b/src/pages/PublicProfilePage.test.jsx
index e89c2d2..7eab23e 100644
--- a/src/pages/PublicProfilePage.test.jsx
+++ b/src/pages/PublicProfilePage.test.jsx
@@ -290,4 +290,69 @@ describe('PublicProfilePage', () => {
delete library.stats;
}
});
+
+ const gamesRoute = {'GET /api/public/profiles/library/stats/games': json(200, {
+ content: [{appid: 1, name: 'Backlogged', status: 'BACKLOG', playtimeMinutes: 0}], page: 0, size: 40, totalElements: 1, totalPages: 1})};
+ const topGamesStats = {pile: null, trend: null, taste: null, finished: null, habits: null,
+ topGames: {mostPlayed: [{appid: 620, name: 'Portal 2', imageUrl: null, minutes: 900}], mostLoyal: null}};
+
+ it('theOwnerOpensTheDetailPageFromTheList', async () => {
+ localStorage.setItem('tbp_session', '1');
+ try {
+ mockBackends({me: {displayName: 'lobo', role: 'USER'}, extra: gamesRoute});
+ renderPage();
+
+ fireEvent.click(await screen.findByRole('button', {name: 'BACKLOG.STATUS.BACKLOG: 142'}));
+
+ expect((await screen.findByRole('link', {name: /Backlogged/})).getAttribute('href')).toBe('/library/1');
+ } finally {
+ localStorage.clear();
+ }
+ });
+
+ it('theOwnerTileRowsOpenTheDetailPage', async () => {
+ localStorage.setItem('tbp_session', '1');
+ library.stats = topGamesStats;
+ try {
+ mockBackends({me: {displayName: 'lobo', role: 'USER'}, style: {tiles: [{id: 'STATS_TOP_GAMES', size: 'M'}]}});
+ renderPage();
+
+ expect((await screen.findByRole('link', {name: /Portal 2/})).getAttribute('href')).toBe('/library/620');
+ } finally {
+ delete library.stats;
+ localStorage.clear();
+ }
+ });
+
+ it('aVisitorSeesTileRowsWithoutLinks', async () => {
+ library.stats = topGamesStats;
+ try {
+ mockBackends({style: {tiles: [{id: 'STATS_TOP_GAMES', size: 'M'}]}});
+ const {container} = renderPage();
+
+ await screen.findByText('Portal 2');
+
+ expect(screen.queryByRole('link', {name: /Portal 2/})).toBeNull();
+ expect(container.querySelector('a[href*="steampowered"]')).toBeNull();
+ } finally {
+ delete library.stats;
+ }
+ });
+
+ it('opensTheGamesBehindPerfectGames', async () => {
+ library.perfectGames = 12;
+ library.averageCompletion = 64.5;
+ try {
+ mockBackends({extra: gamesRoute});
+ renderPage();
+
+ fireEvent.click(await screen.findByRole('button', {name: /PROFILE\.PERFECT_GAMES: 12/}));
+
+ await screen.findByText('Backlogged');
+ expect(fetch.mock.calls.find(([url]) => url.includes('/stats/games'))[0]).toContain('key=PERFECT');
+ } finally {
+ delete library.perfectGames;
+ delete library.averageCompletion;
+ }
+ });
});
diff --git a/src/pages/StatsPage.test.jsx b/src/pages/StatsPage.test.jsx
index 53fbcae..edbf40b 100644
--- a/src/pages/StatsPage.test.jsx
+++ b/src/pages/StatsPage.test.jsx
@@ -113,6 +113,34 @@ describe('StatsPage', () => {
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true&status=BACKLOG');
});
+ it('aZeroTileIsNotClickable', async () => {
+ const base = stats();
+ answerStats(json(200, stats({overview: {...base.overview, neverStarted: 0}})));
+ renderPage();
+
+ await screen.findByText('STATS.OVERVIEW.NEVER_STARTED');
+
+ expect(screen.queryByRole('button', {name: /STATS\.OVERVIEW\.NEVER_STARTED/})).toBeNull();
+ });
+
+ it('zeroCountsAreNotClickableInPileHabitsAndStatuses', async () => {
+ const base = stats();
+ answerStats(json(200, stats({
+ overview: {...base.overview, byStatus: {...base.overview.byStatus, DROPPED: 0}},
+ pile: {...base.pile, backlogGames: 0, gamesWithTimeToBeat: 0},
+ habits: {...base.habits, underOneHourGames: 0},
+ })));
+ renderPage();
+
+ await screen.findByText('STATS.PILE.GAMES');
+
+ for (const name of [/STATS\.PILE\.GAMES/, /STATS\.PILE\.NORMAL/, /STATS\.HABITS\.UNDER_ONE_HOUR/, /BACKLOG\.STATUS\.DROPPED: 0/]) {
+ expect(screen.queryByRole('button', {name})).toBeNull();
+ expect(screen.queryByRole('link', {name})).toBeNull();
+ }
+ expect(screen.getByRole('link', {name: 'BACKLOG.STATUS.PLAYING: 3'})).toBeTruthy();
+ });
+
it('leavesTheMoonTripsAlone', async () => {
answerStats(json(200, stats()));
renderPage();
diff --git a/src/utils/statGames.js b/src/utils/statGames.js
index c777ae9..db3db7d 100644
--- a/src/utils/statGames.js
+++ b/src/utils/statGames.js
@@ -47,8 +47,4 @@ export async function fetchStatGames(source, params, ticket) {
export const libraryLink = (status) => `/library?hideDuplicates=true${status ? `&status=${status}` : ''}`;
-// A visitor has no game page of the owner; the Steam store is the only public one. Manual games have no appid there.
-export const gameHref = (appid, own) => {
- if (own) return `/library/${appid}`;
- return appid > 0 ? `https://store.steampowered.com/app/${appid}` : null;
-};
+export const gameHref = (appid) => `/library/${appid}`;
diff --git a/src/utils/statGames.test.js b/src/utils/statGames.test.js
index fa8941a..2b9b9d2 100644
--- a/src/utils/statGames.test.js
+++ b/src/utils/statGames.test.js
@@ -27,9 +27,8 @@ describe('libraryLink', () => {
});
describe('gameHref', () => {
- it('own_LinksTheLibraryPage', () => expect(gameHref(5, true)).toBe('/library/5'));
- it('visitor_LinksTheStoreOnlyForSteamApps', () => {
- expect(gameHref(5, false)).toBe('https://store.steampowered.com/app/5');
- expect(gameHref(-3, false)).toBeNull();
+ it('linksTheLibraryPage', () => {
+ expect(gameHref(5)).toBe('/library/5');
+ expect(gameHref(-3)).toBe('/library/-3');
});
});