feat(ui): make stat numbers clickable to open games
Build, Test & Push Frontend / quality-check (push) Successful in 28s
Build, Test & Push Frontend / docker (push) Successful in 26s

StatDrill context holds one dialog for a page. StatCount wraps a number:
total and status counts link to the library filter, other numbers open
StatGamesDialog. DrillRow and DrillButton support drill-down in tag bars
(genres, platforms, moods) and other stat displays. Dialog shows 40 games
per page with bottom-sheet layout on phones, handles pagination and errors
(403 not shared, 401 expired, 400 bad argument).
This commit is contained in:
lobo committed 2026-10-03 18:51:59 +02:00
1 parent ed5df8c40d
commit 777ba87fd6
31 files changed
+1026 -261

No files matched your search

+31 -1
View File
@@ -15,12 +15,13 @@ const library = {
};
// `me` only for a test with a session: without one the page asks nothing about the viewer.
function mockBackends({name = 'Lobo', profileStatus = 200, libraryStatus = 200, style, answer = library, me, equipped = {}, rewardsStatus = 200} = {}) {
function mockBackends({name = 'Lobo', profileStatus = 200, libraryStatus = 200, style, answer = library, me, equipped = {}, rewardsStatus = 200, extra = {}} = {}) {
fetchRoutes({
[`GET /api/public/profiles/${encodeURIComponent(name)}`]:
json(profileStatus, {displayName: 'Lobo', memberSince: '2026-03-02', ticket: 'the-ticket', style}),
'GET /api/public/profiles/library': json(libraryStatus, answer),
'GET /api/public/profiles/rewards': json(rewardsStatus, {equipped}),
...extra,
...(me && {
'GET /api/users/me': json(200, me),
'PUT /api/users/settings/profile-tiles': json(204, {}),
@@ -260,4 +261,33 @@ describe('PublicProfilePage', () => {
await waitFor(() => expect(screen.getByText('PROFILE.ERROR_LOAD')).toBeTruthy());
});
it('opensTheGamesBehindAStatusCountWithTheTicket', async () => {
mockBackends({extra: {'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 {container} = renderPage();
fireEvent.click(await screen.findByRole('button', {name: 'BACKLOG.STATUS.BACKLOG: 142'}));
expect(await screen.findByText('Backlogged')).toBeTruthy();
const call = fetch.mock.calls.find(([url]) => url.includes('/stats/games'));
expect(call[0]).toContain('key=STATUS&arg=BACKLOG');
expect(call[1].headers['X-Profile-Ticket']).toBe('the-ticket');
expect(container.querySelector('a[href^="/library"]')).toBeNull();
});
it('rendersNoButtonForAGroupTheOwnerDoesNotShare', async () => {
library.stats = {pile: null, trend: null, topGames: null, taste: null, finished: null,
habits: {moonTrips: 2.5, longestDay: null, underOneHourGames: 0, underOneHourShare: null}};
try {
mockBackends({style: {tiles: [{id: 'STATS_HABITS', size: 'M'}, {id: 'STATS_PILE', size: 'M'}]}});
renderPage();
await screen.findByText('STATS.HABITS.TITLE');
expect(screen.queryByRole('button', {name: /STATS\.PILE/})).toBeNull();
} finally {
delete library.stats;
}
});
});