Public Access
feat(stats): status donut, meters and fixed chart colors
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:
1 parent
dabb0c941c
commit
8aa3181c37
20 files changed
+468
-110
No files matched your search
@@ -114,7 +114,7 @@ describe('StatsPage', () => {
|
||||
answerStats(json(200, stats()));
|
||||
renderPage();
|
||||
|
||||
const link = await screen.findByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 5'});
|
||||
const link = await screen.findByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/});
|
||||
|
||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true&status=BACKLOG');
|
||||
});
|
||||
@@ -140,11 +140,11 @@ describe('StatsPage', () => {
|
||||
|
||||
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/]) {
|
||||
for (const name of [/STATS\.PILE\.GAMES/, /STATS\.PILE\.NORMAL/, /STATS\.HABITS\.UNDER_ONE_HOUR/, /BACKLOG\.STATUS\.DROPPED/]) {
|
||||
expect(screen.queryByRole('button', {name})).toBeNull();
|
||||
expect(screen.queryByRole('link', {name})).toBeNull();
|
||||
}
|
||||
expect(screen.getByRole('link', {name: 'BACKLOG.STATUS.PLAYING: 3'})).toBeTruthy();
|
||||
expect(screen.getByRole('link', {name: /BACKLOG\.STATUS\.PLAYING/})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('leavesTheMoonTripsAlone', async () => {
|
||||
@@ -156,6 +156,129 @@ describe('StatsPage', () => {
|
||||
expect(screen.queryByRole('button', {name: /STATS.HABITS.MOON/})).toBeNull();
|
||||
});
|
||||
|
||||
describe('overview donut', () => {
|
||||
const overview = () => screen.getByText('STATS.OVERVIEW.TITLE').closest('[data-slot="card"]');
|
||||
const paths = () => overview().querySelectorAll('path.stroke-card');
|
||||
|
||||
it('drawsOneColoredSlicePerStatus', async () => {
|
||||
answerStats(json(200, stats()));
|
||||
renderPage();
|
||||
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
expect(paths()).toHaveLength(5);
|
||||
expect(paths()[0].getAttribute('style')).toContain('var(--chart-1)');
|
||||
expect(paths()[4].getAttribute('style')).toContain('var(--chart-5)');
|
||||
});
|
||||
|
||||
it('putsTheTotalInTheHoleAsALink', async () => {
|
||||
answerStats(json(200, stats()));
|
||||
renderPage();
|
||||
|
||||
const link = await within(await screen.findByText('STATS.OVERVIEW.TITLE').then(overview))
|
||||
.findByRole('link', {name: 'STATS.OVERVIEW.GAMES: 12'});
|
||||
|
||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true');
|
||||
});
|
||||
|
||||
it('linksALegendRowToTheStatusLibrary', async () => {
|
||||
answerStats(json(200, stats()));
|
||||
renderPage();
|
||||
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
expect(within(overview()).getByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/}).getAttribute('href'))
|
||||
.toBe('/library?hideDuplicates=true&status=BACKLOG');
|
||||
});
|
||||
|
||||
it('leavesAStatusWithoutGamesWithoutSliceAndTarget', async () => {
|
||||
const base = stats();
|
||||
answerStats(json(200, stats({overview: {...base.overview, byStatus: {...base.overview.byStatus, DROPPED: 0}}})));
|
||||
renderPage();
|
||||
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
expect(paths()).toHaveLength(4);
|
||||
const row = within(overview()).getByText('BACKLOG.STATUS.DROPPED');
|
||||
expect(row.closest('a, button')).toBeNull();
|
||||
});
|
||||
|
||||
it('opensTheStatusLibraryOnASliceClick', async () => {
|
||||
answerStats(json(200, stats()));
|
||||
const {router} = renderPage();
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
fireEvent.click(paths()[1]);
|
||||
|
||||
await waitFor(() => expect(router.state.location.pathname + router.state.location.search)
|
||||
.toBe('/library?hideDuplicates=true&status=PLAYING'));
|
||||
});
|
||||
|
||||
it('showsTheGamesTileInsteadOfTheDonutForAnEmptyLibrary', async () => {
|
||||
const base = stats();
|
||||
answerStats(json(200, stats({overview: {...base.overview, totalGames: 0, playedGames: 0, neverStarted: 0,
|
||||
byStatus: {BACKLOG: 0, PLAYING: 0, FINISHED: 0, COMPLETED: 0, DROPPED: 0}}})));
|
||||
renderPage();
|
||||
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
expect(paths()).toHaveLength(0);
|
||||
expect(within(overview()).getByText('STATS.OVERVIEW.GAMES')).toBeTruthy();
|
||||
expect(within(overview()).queryByRole('link')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('usesThePaletteAndNotTheAccentInAPickedPie', async () => {
|
||||
answerStats(json(200, stats()), {PLATFORMS: 'PIE'});
|
||||
renderPage();
|
||||
|
||||
const card = (await screen.findByText('STATS.PLATFORMS.TITLE')).closest('[data-slot="card"]');
|
||||
await waitFor(() => expect(card.querySelectorAll('path.stroke-card')).toHaveLength(2));
|
||||
const [first, second] = card.querySelectorAll('path.stroke-card');
|
||||
|
||||
expect(first.getAttribute('style')).toContain('var(--chart-1)');
|
||||
expect(second.getAttribute('style')).toContain('var(--chart-2)');
|
||||
});
|
||||
|
||||
it('showsTheUnderOneHourShareAsAMeterThatStaysAButton', async () => {
|
||||
answerStats(json(200, stats()));
|
||||
renderPage();
|
||||
|
||||
const meter = (await screen.findAllByRole('meter', {hidden: true}))[0];
|
||||
|
||||
expect(meter.getAttribute('aria-valuenow')).toBe('13');
|
||||
expect(meter.getAttribute('aria-label')).toBe('STATS.HABITS.UNDER_ONE_HOUR');
|
||||
expect(meter.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(screen.getByRole('button', {name: /STATS\.HABITS\.UNDER_ONE_HOUR/})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keepsTheMeterOfAnUnderOneHourShareWithoutGamesButDropsTheButton', async () => {
|
||||
const base = stats();
|
||||
answerStats(json(200, stats({habits: {...base.habits, underOneHourGames: 0, underOneHourShare: 0}})));
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByRole('meter', {name: 'STATS.HABITS.UNDER_ONE_HOUR'})).toBeTruthy();
|
||||
expect(screen.queryByRole('button', {name: /STATS\.HABITS\.UNDER_ONE_HOUR/})).toBeNull();
|
||||
});
|
||||
|
||||
it('drawsABarUnderEachMostPlayedGame', async () => {
|
||||
fetchRoutes({
|
||||
'GET /api/library/stats': json(200, stats()),
|
||||
'GET /api/users/me': me(),
|
||||
'GET /api/library/stats/games': json(200, {content: [], page: 0, size: 40, totalElements: 0, totalPages: 0}),
|
||||
});
|
||||
renderPage();
|
||||
|
||||
const card = (await screen.findByText('STATS.TOP_GAMES.TITLE')).closest('[data-slot="card"]');
|
||||
const [ranked, loyal] = within(card).getAllByRole('link', {name: /Portal 2/});
|
||||
|
||||
expect(ranked.getAttribute('href')).toBe('/library/620');
|
||||
expect(ranked.querySelectorAll('[aria-hidden="true"] > .bg-primary')).toHaveLength(1);
|
||||
expect(loyal.querySelectorAll('[aria-hidden="true"] > .bg-primary')).toHaveLength(0);
|
||||
fireEvent.click(within(card).getByRole('button', {name: 'STATS.DRILL.SHOW_ALL'}));
|
||||
expect(await screen.findByRole('dialog')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('chart forms', () => {
|
||||
const section = (title) => screen.getByText(title).closest('[data-slot="card"]');
|
||||
const picker = (title) => within(section(title)).getByRole('group', {name: 'STATS.CHART.LABEL'});
|
||||
|
||||
Reference in new issue
Block a user