Public Access
fix(stats): open game detail pages from stat lists, perfect games clickable
Add perfect games click-through on the dashboard. Remove Steam store links from game lists. Stat numbers with count 0 are not clickable. Tests cover the perfect-games click, zero guards, and owner versus visitor behavior.
This commit is contained in:
1 parent
777ba87fd6
commit
8b94a25844
27 files changed
+270
-51
No files matched your search
@@ -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 = <FitText text={game.name ?? `App ${game.appid}`}/>;
|
||||
return (
|
||||
<span className="@container min-w-0 flex-1 font-medium">
|
||||
{href ? <a href={href} target="_blank" rel="noopener noreferrer" className="hover:text-primary">{name}</a> : name}
|
||||
<FitText text={game.name ?? `App ${game.appid}`}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function PublicProfilePage() {
|
||||
&& <Playing games={library.playing.slice(0, PLAYING_GAMES[size])} compact={size === 'S'}/>;
|
||||
case 'FINISHED':
|
||||
return library.finished?.length > 0 && <Finished games={library.finished.slice(0, FINISHED_GAMES[size])} equipped={equipped}/>;
|
||||
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() {
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
<StatDrillProvider source="profile" ticket={profile.ticket}>
|
||||
<StatDrillProvider source="profile" ticket={profile.ticket} linked={owner}>
|
||||
<TileGrid tiles={PROFILE_TILES} titles={PROFILE_TITLES} layout={layout} renderTile={renderTile}
|
||||
editing={owner && editing} defaultLayout={DEFAULT_PROFILE} onSave={saveLayout}
|
||||
onCancel={() => setEditing(false)}/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user