feat(stats): add stats page and pile trend
Build, Test & Push Frontend / quality-check (push) Successful in 15s
Build, Test & Push Frontend / docker (push) Successful in 22s

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:
lobo committed 2026-09-27 07:16:10 +02:00
1 parent 83bbc2730d
commit 9d1c7bb40d
31 files changed
+978 -11

No files matched your search

+51
View File
@@ -55,6 +55,57 @@ describe('SettingsPage public profile', () => {
await waitFor(() => expect(screen.queryByText(/\/u\/Lobo$/)).toBeNull());
});
it('saves the stat groups the public profile shows, in a fixed order', async () => {
renderPage(profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: ['TREND']}}));
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
await waitFor(() => expect(puts()).toHaveLength(1));
expect(puts()[0]).toEqual({groups: ['PILE', 'TREND']});
const [url] = fetch.mock.calls.find(([, init]) => init?.method === 'PUT');
expect(url).toMatch(/\/api\/users\/settings\/profile-stats$/);
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.PILE.TITLE'}).getAttribute('aria-checked'))
.toBe('true'));
});
it('waits for a save before the next switch click', async () => {
const answers = [];
const me = profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: []}});
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
? new Promise((resolve) => answers.push(() => resolve({ok: true, status: 204, json: async () => ({})})))
: Promise.resolve({ok: true, status: 200, json: async () => me})));
render(<MemoryRouter><PreferencesProvider><SettingsPage/></PreferencesProvider></MemoryRouter>);
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}).disabled).toBe(true));
answers.forEach((answer) => answer());
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}).disabled).toBe(false));
fireEvent.click(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}));
await waitFor(() => expect(puts()).toHaveLength(2));
expect(puts()[1]).toEqual({groups: ['PILE', 'TASTE']});
});
it('switches the group back when the save fails', async () => {
const me = profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: []}});
globalThis.fetch = vi.fn(async (url, init) => (init?.method === 'PUT'
? {ok: false, status: 500, json: async () => ({})}
: {ok: true, status: 200, json: async () => me}));
render(<MemoryRouter><PreferencesProvider><SettingsPage/></PreferencesProvider></MemoryRouter>);
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.PILE.TITLE'}).getAttribute('aria-checked'))
.toBe('false'));
});
it('offers the stat groups only while the profile is public', async () => {
renderPage(profile({settings: {useDarkTheme: true, language: 'de', publicProfile: false}}));
expect((await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'})).disabled).toBe(true);
expect(screen.getByText('SETTINGS.PROFILE_STATS_NEEDS_PUBLIC')).toBeTruthy();
});
it('needs a display name first', async () => {
renderPage(profile({displayName: null, settings: {useDarkTheme: true, language: 'de', publicProfile: false}}));