// @vitest-environment jsdom import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {cleanup, fireEvent, screen, waitFor, within} from '@testing-library/react'; import StatsPage from './StatsPage.jsx'; import {renderWithProviders} from '../test/renderWithProviders.jsx'; import {fetchRoutes, json} from '../test/fetchRoutes.js'; vi.mock('react-i18next', () => import('../test/i18n-mock.js')); const me = (statCharts) => json(200, {email: 'a@b.de', settings: {statCharts}}); const answerStats = (answer, statCharts) => fetchRoutes({ 'GET /api/library/stats': answer, 'GET /api/users/me': me(statCharts), 'PUT /api/users/settings/stat-chart': json(204, {}), }); 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}, platforms: {perPlatform: [{name: 'Steam', games: 10, minutes: 5000}, {name: 'PS5', games: 2, minutes: 600}]}, 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 = () => renderWithProviders(); describe('StatsPage', () => { beforeEach(() => { localStorage.setItem('tbp_session', '1'); }); afterEach(() => { cleanup(); localStorage.clear(); }); it('asks for every group once and shows each of them', async () => { answerStats(json(200, stats())); renderPage(); expect(await screen.findByText('STATS.OVERVIEW.TITLE')).toBeTruthy(); const statsCalls = fetch.mock.calls.filter(([url]) => url.includes('/api/library/stats')); expect(statsCalls).toHaveLength(1); expect(statsCalls[0][0]).toMatch(/\/api\/library\/stats$/); for (const title of ['PILE', 'TREND', 'TOP_GAMES', 'TASTE', 'HABITS', 'FINISHED', 'PLATFORMS']) { expect(screen.getByText(`STATS.${title}.TITLE`)).toBeTruthy(); } expect(screen.getAllByText('Portal 2')).toHaveLength(2); expect(screen.getByText('Half-Life 2')).toBeTruthy(); expect(screen.getByText('PS5')).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(); answerStats(json(200, { ...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}, platforms: {perPlatform: []}, 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.PLATFORMS.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 () => { answerStats(json(500, {})); renderPage(); fireEvent.click(await screen.findByRole('button', {name: /DASHBOARD.RETRY/})); await waitFor(() => expect(fetch.mock.calls.filter(([url]) => url.includes('/api/library/stats'))).toHaveLength(2)); }); it('opensTheGamesBehindARowWithTheRawName', async () => { fetchRoutes({ 'GET /api/library/stats': json(200, stats()), 'GET /api/users/me': me(), 'GET /api/library/stats/games': json(200, {content: [{appid: 620, name: 'Portal 2', status: 'BACKLOG', playtimeMinutes: 60}], page: 0, size: 40, totalElements: 1, totalPages: 1}), }); renderPage(); fireEvent.click(await screen.findByRole('button', {name: /STATS\.GENRE/})); expect(await screen.findByRole('dialog')).toBeTruthy(); await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/stats\/games\?key=GENRE&arg=Shooter&page=0$/)); }); it('linksAStatusCountToTheFilteredLibrary', async () => { answerStats(json(200, stats())); renderPage(); const link = await screen.findByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/}); expect(link.getAttribute('href')).toBe('/library?status=BACKLOG'); }); it('aZeroTileIsNotClickable', async () => { const base = stats(); answerStats(json(200, stats({overview: {...base.overview, neverStarted: 0}}))); renderPage(); await screen.findByText('STATS.OVERVIEW.NEVER_STARTED'); expect(screen.queryByRole('button', {name: /STATS\.OVERVIEW\.NEVER_STARTED/})).toBeNull(); }); it('zeroCountsAreNotClickableInPileHabitsAndStatuses', async () => { const base = stats(); answerStats(json(200, stats({ overview: {...base.overview, byStatus: {...base.overview.byStatus, DROPPED: 0}}, pile: {...base.pile, backlogGames: 0, gamesWithTimeToBeat: 0}, habits: {...base.habits, underOneHourGames: 0}, }))); renderPage(); await screen.findByText('STATS.PILE.GAMES'); 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/})).toBeTruthy(); }); it('leavesTheMoonTripsAlone', async () => { answerStats(json(200, stats())); renderPage(); await screen.findByText('STATS.HABITS.MOON'); 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'); }); 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?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?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'}); const putBodies = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT').map(([, init]) => JSON.parse(init.body)); const slices = (title) => section(title).querySelectorAll('path.stroke-card'); it('showsBarsAsTodayAndAPickerPerChartableGroup', async () => { answerStats(json(200, stats())); renderPage(); await screen.findByText('STATS.PLATFORMS.TITLE'); expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(0); for (const title of ['PLATFORMS', 'TASTE', 'FINISHED', 'TREND', 'TOP_GAMES']) { expect(within(picker(`STATS.${title}.TITLE`)).getAllByRole('button').length).toBeGreaterThan(1); } expect(within(picker('STATS.TOP_GAMES.TITLE')).getByRole('button', {name: 'STATS.CHART.LIST'}).getAttribute('aria-pressed')).toBe('true'); expect(within(picker('STATS.PLATFORMS.TITLE')).getByRole('button', {name: 'STATS.CHART.BARS'}).getAttribute('aria-pressed')).toBe('true'); }); it('savesThePickedFormAndSavesNullForTheDefault', async () => { answerStats(json(200, stats())); renderPage(); await screen.findByText('STATS.PLATFORMS.TITLE'); fireEvent.click(within(picker('STATS.PLATFORMS.TITLE')).getByRole('button', {name: 'STATS.CHART.PIE'})); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(2)); expect(putBodies()).toEqual([{group: 'PLATFORMS', chart: 'PIE'}]); fireEvent.click(within(picker('STATS.PLATFORMS.TITLE')).getByRole('button', {name: 'STATS.CHART.BARS'})); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(0)); expect(putBodies().at(-1)).toEqual({group: 'PLATFORMS', chart: null}); }); const twoGames = () => stats({topGames: { mostPlayed: [{appid: 620, name: 'Portal 2', imageUrl: null, minutes: 900}, {appid: 220, name: 'Half-Life 2', imageUrl: null, minutes: 300}], mostLoyal: {appid: 620, name: 'Portal 2', imageUrl: null, months: 3, minutes: 900}}}); it('topGamesPieLinksEachGameAndShowsTheShareNote', async () => { answerStats(json(200, twoGames())); renderPage(); await screen.findByText('STATS.TOP_GAMES.TITLE'); fireEvent.click(within(picker('STATS.TOP_GAMES.TITLE')).getByRole('button', {name: 'STATS.CHART.PIE'})); await waitFor(() => expect(slices('STATS.TOP_GAMES.TITLE')).toHaveLength(2)); expect(putBodies()).toEqual([{group: 'TOP_GAMES', chart: 'PIE'}]); const card = section('STATS.TOP_GAMES.TITLE'); expect(within(card).getByRole('link', {name: /Half-Life 2/}).getAttribute('href')).toBe('/library/220'); expect(within(card).getByRole('link', {name: /Half-Life 2/}).textContent).toContain('5.0h'); expect(within(card).getByText('STATS.TOP_GAMES.SHARE')).toBeTruthy(); expect(within(card).getByText('STATS.TOP_GAMES.LOYAL')).toBeTruthy(); }); it('topGamesPieWithOneGameDrawsTheList', async () => { answerStats(json(200, stats()), {TOP_GAMES: 'PIE'}); renderPage(); await screen.findByText('STATS.TOP_GAMES.TITLE'); expect(slices('STATS.TOP_GAMES.TITLE')).toHaveLength(0); expect(section('STATS.TOP_GAMES.TITLE').querySelectorAll('img').length).toBeGreaterThan(0); }); it('topGamesBarsHaveNoCoversAndLinkTheGame', async () => { answerStats(json(200, twoGames()), {TOP_GAMES: 'BARS'}); renderPage(); await screen.findByText('STATS.TOP_GAMES.TITLE'); const card = section('STATS.TOP_GAMES.TITLE'); const bars = within(card).getAllByRole('link', {name: /Half-Life 2/}); expect(bars[0].getAttribute('href')).toBe('/library/220'); expect(bars[0].querySelector('img')).toBeNull(); expect(bars[0].querySelectorAll('.bg-primary')).toHaveLength(1); }); it('showsTheStoredFormOfTheUser', async () => { answerStats(json(200, stats()), {PLATFORMS: 'DONUT', TREND: 'BARS'}); renderPage(); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(2)); expect(screen.queryByTestId('pile-trend')).toBeNull(); }); it('putsTheOldFormBackWhenTheSaveFails', async () => { fetchRoutes({ 'GET /api/library/stats': json(200, stats()), 'GET /api/users/me': me(), 'PUT /api/users/settings/stat-chart': json(500, {}), }); renderPage(); await screen.findByText('STATS.PLATFORMS.TITLE'); fireEvent.click(within(picker('STATS.PLATFORMS.TITLE')).getByRole('button', {name: 'STATS.CHART.PIE'})); await waitFor(() => expect(putBodies()).toHaveLength(1)); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(0)); expect(within(picker('STATS.PLATFORMS.TITLE')).getByRole('button', {name: 'STATS.CHART.BARS'}).getAttribute('aria-pressed')).toBe('true'); }); it('aLegendRowOpensTheGamesWithThePlatform', async () => { fetchRoutes({ 'GET /api/library/stats': json(200, stats()), 'GET /api/users/me': me({PLATFORMS: 'PIE'}), 'GET /api/library/stats/games': json(200, {content: [], page: 0, size: 40, totalElements: 0, totalPages: 0}), }); renderPage(); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(2)); fireEvent.click(await within(section('STATS.PLATFORMS.TITLE')).findByRole('button', {name: /PS5/})); expect(await screen.findByRole('dialog')).toBeTruthy(); await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/stats\/games\?key=PLATFORM&arg=PS5&page=0$/)); }); it('theOtherSliceIsNotAButtonAndHoldsTheRest', async () => { const perPlatform = Array.from({length: 8}, (_, i) => ({name: `P${i}`, games: 8 - i, minutes: 10})); answerStats(json(200, stats({platforms: {perPlatform}})), {PLATFORMS: 'PIE'}); renderPage(); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(6)); const legend = within(section('STATS.PLATFORMS.TITLE')); expect(legend.getAllByRole('button', {name: /P\d/})).toHaveLength(5); expect(legend.getByText('STATS.CHART.OTHER').closest('button')).toBeNull(); expect(legend.getByText('STATS.TASTE.GAMES:6')).toBeTruthy(); }); it('aPlatformRowWithoutGamesIsNoButtonInBarsAndList', async () => { const perPlatform = [{name: 'Steam', games: 3, minutes: 5}, {name: 'Switch', games: 0, minutes: 0}]; for (const form of ['BARS', 'LIST']) { answerStats(json(200, stats({platforms: {perPlatform}})), {PLATFORMS: form}); renderPage(); const platforms = within(await screen.findByText('STATS.PLATFORMS.TITLE').then(() => section('STATS.PLATFORMS.TITLE'))); expect(platforms.getByRole('button', {name: /Steam/})).toBeTruthy(); expect(platforms.getByText('Switch')).toBeTruthy(); expect(platforms.queryByRole('button', {name: /Switch/})).toBeNull(); cleanup(); } }); it('aPlatformWithoutGamesGetsNoSlice', async () => { answerStats(json(200, stats({platforms: {perPlatform: [ {name: 'Steam', games: 3, minutes: 5}, {name: 'Switch', games: 0, minutes: 0}]}})), {PLATFORMS: 'PIE'}); renderPage(); await waitFor(() => expect(slices('STATS.PLATFORMS.TITLE')).toHaveLength(1)); expect(within(section('STATS.PLATFORMS.TITLE')).queryByText('Switch')).toBeNull(); }); it('showsTheEmptyTextForEveryForm', async () => { answerStats(json(200, stats({platforms: {perPlatform: []}})), {PLATFORMS: 'PIE'}); renderPage(); expect(await screen.findByText('STATS.PLATFORMS.EMPTY')).toBeTruthy(); }); it('aLineYearOpensItsGamesAndAYearWithoutGamesIsNoButton', async () => { fetchRoutes({ 'GET /api/library/stats': json(200, stats({finished: {finishedBeforeTracking: 0, perYear: [{year: 2022, games: 3}, {year: 2024, games: 1}]}})), 'GET /api/users/me': me({FINISHED: 'LINE'}), 'GET /api/library/stats/games': json(200, {content: [], page: 0, size: 40, totalElements: 0, totalPages: 0}), }); renderPage(); const finished = within(await screen.findByText('STATS.FINISHED.TITLE').then(() => section('STATS.FINISHED.TITLE'))); await waitFor(() => expect(finished.getByRole('button', {name: /2022/})).toBeTruthy()); expect(finished.getByRole('button', {name: /2024/})).toBeTruthy(); expect(finished.queryByRole('button', {name: /2023/})).toBeNull(); expect(finished.getByText('2023: 0')).toBeTruthy(); const circles = [...section('STATS.FINISHED.TITLE').querySelectorAll('circle')]; expect(circles).toHaveLength(3); expect(circles.filter((c) => c.getAttribute('class').includes('cursor-pointer'))).toHaveLength(2); fireEvent.click(finished.getByRole('button', {name: /2022/})); await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/stats\/games\?key=FINISHED_YEAR&arg=2022&page=0$/)); }); it('aLineWithOneYearDrawsBars', async () => { answerStats(json(200, stats()), {FINISHED: 'LINE'}); renderPage(); const finished = within(await screen.findByText('STATS.FINISHED.TITLE').then(() => section('STATS.FINISHED.TITLE'))); await waitFor(() => expect(finished.getByRole('button', {name: /2026/})).toBeTruthy()); expect(section('STATS.FINISHED.TITLE').querySelector('polyline')).toBeNull(); }); it('aDonutOfTheTasteOpensTheGenreWithItsRawNameAndShowsTheOverlapNote', async () => { fetchRoutes({ 'GET /api/library/stats': json(200, stats()), 'GET /api/users/me': me({TASTE: 'DONUT'}), 'GET /api/library/stats/games': json(200, {content: [], page: 0, size: 40, totalElements: 0, totalPages: 0}), }); renderPage(); const taste = within(await screen.findByText('STATS.TASTE.TITLE').then(() => section('STATS.TASTE.TITLE'))); await waitFor(() => expect(taste.getAllByText('STATS.CHART.OVERLAP')).toHaveLength(2)); fireEvent.click(taste.getByRole('button', {name: /STATS\.GENRE/})); await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/stats\/games\?key=GENRE&arg=Shooter&page=0$/)); }); it('theTrendBarsStayOneClickTargetWithAListOfTheDays', async () => { answerStats(json(200, stats()), {TREND: 'BARS'}); renderPage(); const trend = within(await screen.findByText('STATS.TREND.TITLE').then(() => section('STATS.TREND.TITLE'))); await waitFor(() => expect(section('STATS.TREND.TITLE').querySelectorAll('rect')).toHaveLength(2)); expect(trend.getAllByRole('button', {name: 'STATS.DRILL.TREND_MOVES'})).toHaveLength(1); expect(section('STATS.TREND.TITLE').querySelectorAll('table tr')).toHaveLength(2); }); // REVIEW-FINDING: the picker buttons sit inside the h2, so the heading name becomes "title + Chart Bars Pie ..." it('reviewFinding_thePickerIsNotPartOfTheHeadingName', async () => { answerStats(json(200, stats())); renderPage(); expect(await screen.findByRole('heading', {name: 'STATS.PLATFORMS.TITLE'})).toBeTruthy(); }); // REVIEW-FINDING: an empty tag list in pie/donut form draws an empty chart box and the overlap note it('reviewFinding_anEmptyMoodListInThePieShowsNoChartAndNoNote', async () => { answerStats(json(200, stats({taste: {genres: [{name: 'Shooter', games: 2, minutes: 1200}], moods: []}})), {TASTE: 'PIE'}); renderPage(); await waitFor(() => expect(slices('STATS.TASTE.TITLE')).toHaveLength(1)); expect(within(section('STATS.TASTE.TITLE')).getAllByText('STATS.CHART.OVERLAP')).toHaveLength(1); }); }); });