feat(stats): status donut, meters and fixed chart colors
Build, Test & Push Frontend / quality-check (push) Successful in 35s
Build, Test & Push Frontend / docker (push) Successful in 22s

Pie and donut charts use one fixed color-blind safe palette in every
skin and accent. The overview and the profile status tile show a status
donut with the total in the middle. Habits and achievements get meter
bars, top games get share bars, the pile of shame bar uses the palette.
This commit is contained in:
lobo committed 2026-10-04 12:03:52 +02:00
1 parent dabb0c941c
commit 8aa3181c37
20 files changed
+468 -110

No files matched your search

+53 -4
View File
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
import {cleanup, fireEvent, screen, waitFor} from '@testing-library/react';
import {cleanup, fireEvent, screen, waitFor, within} from '@testing-library/react';
import PublicProfilePage from './PublicProfilePage.jsx';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
import {renderWithProviders} from '../test/renderWithProviders.jsx';
@@ -98,7 +98,7 @@ describe('PublicProfilePage', () => {
expect(fetch.mock.calls[1][1].headers['X-Profile-Ticket']).toBe('the-ticket');
expect(screen.getByText('Lobo')).toBeTruthy();
expect(screen.getByText('Half-Life 2')).toBeTruthy();
expect(screen.getByText('233')).toBeTruthy();
expect(screen.getAllByText('233')).toHaveLength(2);
expect(screen.queryByText('PROFILE.PERFECT_GAMES')).toBeNull();
});
@@ -267,7 +267,7 @@ describe('PublicProfilePage', () => {
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'}));
fireEvent.click(await screen.findByRole('button', {name: /BACKLOG\.STATUS\.BACKLOG/}));
expect(await screen.findByText('Backlogged')).toBeTruthy();
const call = fetch.mock.calls.find(([url]) => url.includes('/stats/games'));
@@ -293,6 +293,55 @@ describe('PublicProfilePage', () => {
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})};
it('drawsTheStatusTileAsADonut', async () => {
mockBackends();
const {container} = renderPage();
await screen.findByText('Portal 2');
expect(container.querySelectorAll('[data-tile="STATUS"] path.stroke-card')).toHaveLength(5);
});
it('showsTheTotalInTheDonutAsTextWithoutTheSummarySection', async () => {
mockBackends({answer: {...library, totalGames: null}});
const {container} = renderPage();
await screen.findByText('Portal 2');
const tile = container.querySelector('[data-tile="STATUS"]');
expect(within(tile).getByText('233')).toBeTruthy();
expect(within(tile).queryByRole('button', {name: /STATS\.OVERVIEW\.GAMES/})).toBeNull();
});
it('opensAllGamesFromTheDonutTotalWhenTheSummaryIsShared', async () => {
mockBackends({extra: gamesRoute});
const {container} = renderPage();
await screen.findByText('Portal 2');
fireEvent.click(within(container.querySelector('[data-tile="STATUS"]')).getByRole('button', {name: 'STATS.OVERVIEW.GAMES: 233'}));
await screen.findByText('Backlogged');
expect(fetch.mock.calls.find(([url]) => url.includes('/stats/games'))[0]).toContain('key=TOTAL');
});
it('showsTheAverageCompletionAsAMeter', async () => {
mockBackends({answer: {...library, averageCompletion: 71.3, perfectGames: 2}});
renderPage();
const meter = await screen.findByRole('meter', {name: 'PROFILE.AVERAGE_COMPLETION'});
expect(meter.getAttribute('aria-valuetext')).toBe('71,3 %');
});
it('showsNoMeterWithoutAnAverageCompletion', async () => {
mockBackends();
renderPage();
await screen.findByText('PROFILE.GAMES');
expect(screen.queryByRole('meter')).toBeNull();
});
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}};
@@ -302,7 +351,7 @@ describe('PublicProfilePage', () => {
mockBackends({me: {displayName: 'lobo', role: 'USER'}, extra: gamesRoute});
renderPage();
fireEvent.click(await screen.findByRole('button', {name: 'BACKLOG.STATUS.BACKLOG: 142'}));
fireEvent.click(await screen.findByRole('button', {name: /BACKLOG\.STATUS\.BACKLOG/}));
expect((await screen.findByRole('link', {name: /Backlogged/})).getAttribute('href')).toBe('/library/1');
} finally {