Public Access
feat(stats): add stats page and pile trend
Show the stat groups on /stats, the pile trend on the dashboard card, one switch per group in the settings, and the shared groups on the public profile.
This commit is contained in:
1 parent
83bbc2730d
commit
9d1c7bb40d
31 files changed
+978
-11
No files matched your search
@@ -0,0 +1,87 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import StatsPage from './StatsPage.jsx';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
const json = (body, status = 200) => ({ok: status < 400, status, json: async () => body});
|
||||
|
||||
const stats = (extra = {}) => ({
|
||||
overview: {totalGames: 12, totalPlaytimeMinutes: 6000, playedGames: 8, neverStarted: 4,
|
||||
byStatus: {BACKLOG: 5, PLAYING: 3, FINISHED: 2, COMPLETED: 1, DROPPED: 1}},
|
||||
pile: {backlogGames: 5, normalMinutes: 4200, completionistMinutes: 9000, gamesWithTimeToBeat: 4,
|
||||
minutesPerWeek: 420, paceWeeks: 10, workYears: 0.03,
|
||||
oldestUnplayed: {appid: 220, name: 'Half-Life 2', imageUrl: null, inLibrarySince: '2026-01-01T00:00:00Z'}},
|
||||
topGames: {mostPlayed: [{appid: 620, name: 'Portal 2', imageUrl: null, minutes: 900}],
|
||||
mostLoyal: {appid: 620, name: 'Portal 2', imageUrl: null, months: 3, minutes: 900}},
|
||||
taste: {genres: [{name: 'Shooter', games: 2, minutes: 1200}], moods: [{name: 'CHILL', games: 1, minutes: 900}]},
|
||||
habits: {moonTrips: 0.5, longestDay: {day: '2026-07-01', minutes: 200}, underOneHourGames: 1, underOneHourShare: 0.125},
|
||||
finished: {perYear: [{year: 2026, games: 2}], finishedBeforeTracking: 1},
|
||||
trend: {days: [{day: '2026-09-26', backlogGames: 6, neverPlayed: 4, backlogMinutes: 5000},
|
||||
{day: '2026-09-27', backlogGames: 5, neverPlayed: 4, backlogMinutes: 4200}], thisMonth: {added: 1, done: 2}},
|
||||
...extra,
|
||||
});
|
||||
|
||||
const renderPage = () => render(<MemoryRouter><PreferencesProvider><StatsPage/></PreferencesProvider></MemoryRouter>);
|
||||
|
||||
describe('StatsPage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('asks for every group once and shows each of them', async () => {
|
||||
globalThis.fetch = vi.fn(async () => json(stats()));
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('STATS.OVERVIEW.TITLE')).toBeTruthy();
|
||||
expect(fetch).toHaveBeenCalledOnce();
|
||||
expect(fetch.mock.calls[0][0]).toMatch(/\/api\/library\/stats$/);
|
||||
for (const title of ['PILE', 'TREND', 'TOP_GAMES', 'TASTE', 'HABITS', 'FINISHED']) {
|
||||
expect(screen.getByText(`STATS.${title}.TITLE`)).toBeTruthy();
|
||||
}
|
||||
expect(screen.getAllByText('Portal 2')).toHaveLength(2);
|
||||
expect(screen.getByText('Half-Life 2')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.PILE.PACE')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.FINISHED.BEFORE:1')).toBeTruthy();
|
||||
expect(screen.getByTestId('pile-trend')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the empty texts of a new library', async () => {
|
||||
const empty = stats();
|
||||
globalThis.fetch = vi.fn(async () => json({
|
||||
...empty,
|
||||
pile: {...empty.pile, paceWeeks: null, oldestUnplayed: null},
|
||||
topGames: {mostPlayed: [], mostLoyal: null},
|
||||
taste: {genres: [], moods: []},
|
||||
habits: {moonTrips: 0, longestDay: null, underOneHourGames: 0, underOneHourShare: null},
|
||||
finished: {perYear: [], finishedBeforeTracking: 0},
|
||||
trend: {days: [empty.trend.days[1]], thisMonth: {added: 0, done: 0}},
|
||||
}));
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('STATS.PILE.PACE_NONE')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.TOP_GAMES.EMPTY')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.TASTE.EMPTY')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.FINISHED.EMPTY')).toBeTruthy();
|
||||
expect(screen.getByText('STATS.TREND.STARTS')).toBeTruthy();
|
||||
expect(screen.queryByText('STATS.HABITS.LONGEST_DAY')).toBeNull();
|
||||
expect(screen.queryByTestId('pile-trend')).toBeNull();
|
||||
});
|
||||
|
||||
it('offers a retry when the stats could not be loaded', async () => {
|
||||
globalThis.fetch = vi.fn(async () => json({}, 500));
|
||||
renderPage();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: /DASHBOARD.RETRY/}));
|
||||
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user