Public Access
perf(library): one dashboard call, stored game detail first
The Dashboard loads the top and playing tiles from GET /api/library/dashboard-games in one call. The game page shows the stored game first (refresh=false) and calls again only when achievementsStale is true. The second answer merges the achievement fields; it takes status and finishedHint only when the user did not change the status. The points check runs again after the refresh. Refs #47
This commit is contained in:
1 parent
73ad4fd3b8
commit
0924600d95
10 files changed
+179
-36
No files matched your search
@@ -12,9 +12,10 @@ const waitingKey = (kind, reason) => (kind === 'FINISHED_TOPUP' ? WAITING_TOPUP.
|
||||
|
||||
/**
|
||||
* What the game earned: one line per paid row and one per kind that is missing, with the reason from Core. Loading it
|
||||
* settles the points; a status change after the first load also shows the points toast.
|
||||
* settles the points; a status change after the first load also shows the points toast. `refreshes` loads it again
|
||||
* after the Steam refresh of the game page, which may have stored an unlock that settles points.
|
||||
*/
|
||||
export default function GamePointsLine({game}) {
|
||||
export default function GamePointsLine({game, refreshes = 0}) {
|
||||
const {t, i18n} = useTranslation();
|
||||
const check = usePointsCheck();
|
||||
const [points, setPoints] = useState(null);
|
||||
@@ -30,7 +31,7 @@ export default function GamePointsLine({game}) {
|
||||
return () => {
|
||||
stale = true;
|
||||
};
|
||||
}, [check, game.appid, game.status]);
|
||||
}, [check, game.appid, game.status, refreshes]);
|
||||
|
||||
if (!points) return null;
|
||||
const earlySinceText = points.earlySince && formatDay(points.earlySince, i18n.language, {dateStyle: 'medium'});
|
||||
|
||||
@@ -3,10 +3,8 @@ import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
|
||||
import {request} from '../utils/api.js';
|
||||
import {PLAYING_GAMES, TOP_GAMES, UP_NEXT_GAMES} from '../utils/tiles.js';
|
||||
|
||||
// The games endpoint accepts only the page sizes 20, 40 and 80.
|
||||
const TOP_GAMES_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc';
|
||||
const DASHBOARD_GAMES_URL = `/api/library/dashboard-games?top=${TOP_GAMES.L}&playing=${PLAYING_GAMES.L}`;
|
||||
const UP_NEXT_URL = `/api/library/backlog?limit=${UP_NEXT_GAMES.L}&released=true`;
|
||||
const PLAYING_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc&status=PLAYING';
|
||||
|
||||
// A card whose request fails or has no JSON body stays empty.
|
||||
async function loadInto(path, set, pick = (data) => data) {
|
||||
@@ -30,8 +28,10 @@ export default function useDashboardData() {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchSteamCount = useCallback(() => loadInto('/api/library/games-count', setSteamData), []);
|
||||
const fetchTopGames = useCallback(() => loadInto(TOP_GAMES_URL, setTopGames, (page) => (page.content ?? []).slice(0, TOP_GAMES.L)), []);
|
||||
const fetchPlaying = useCallback(() => loadInto(PLAYING_URL, setPlaying, (page) => (page.content ?? []).slice(0, PLAYING_GAMES.L)), []);
|
||||
const fetchGames = useCallback(() => loadInto(DASHBOARD_GAMES_URL, (data) => {
|
||||
setTopGames(data.top ?? []);
|
||||
setPlaying(data.playing ?? []);
|
||||
}), []);
|
||||
const fetchUpNext = useCallback(() => loadInto(UP_NEXT_URL, setUpNext, (data) => data.games ?? []), []);
|
||||
const fetchActivity = useCallback(() => loadInto('/api/library/activity', setActivity), []);
|
||||
const fetchAchievements = useCallback(() => loadInto('/api/library/achievements/summary', setAchievements), []);
|
||||
@@ -50,8 +50,7 @@ export default function useDashboardData() {
|
||||
|
||||
const reloadLibrary = () => {
|
||||
fetchSteamCount();
|
||||
fetchTopGames();
|
||||
fetchPlaying();
|
||||
fetchGames();
|
||||
fetchUpNext();
|
||||
fetchSummary();
|
||||
fetchActivity();
|
||||
@@ -65,7 +64,7 @@ export default function useDashboardData() {
|
||||
const withHistory = fetchSteamCount().then(async (counts) => {
|
||||
if (counts == null || counts.canSync) await Promise.all([fetchActivity(), fetchAchievements()]);
|
||||
});
|
||||
const [{ok, status}] = await Promise.all([reloadUser(), withHistory, fetchTopGames(), fetchPlaying(),
|
||||
const [{ok, status}] = await Promise.all([reloadUser(), withHistory, fetchGames(),
|
||||
fetchUpNext(), fetchSummary()]);
|
||||
if (!ok) {
|
||||
if (status === 0) setError('INFO.ERROR.NETWORK_FAILURE');
|
||||
@@ -75,7 +74,7 @@ export default function useDashboardData() {
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}, [reloadUser, fetchSteamCount, fetchTopGames, fetchPlaying, fetchUpNext, fetchSummary, fetchActivity, fetchAchievements]);
|
||||
}, [reloadUser, fetchSteamCount, fetchGames, fetchUpNext, fetchSummary, fetchActivity, fetchAchievements]);
|
||||
|
||||
return {
|
||||
steamData, topGames, playing, upNext, summary, summaryFailed, activity, achievements, isLoading, error,
|
||||
|
||||
+23
-3
@@ -2,11 +2,17 @@ import {useCallback, useEffect, useState} from 'react';
|
||||
import {request} from '../utils/api.js';
|
||||
import useAbortPrevious from './useAbortPrevious.js';
|
||||
|
||||
// What the Steam refresh changes. The rest of the refreshed answer was read before a save made during the wait.
|
||||
const ACHIEVEMENT_FIELDS = ['hasAchievements', 'statsVisible', 'achievementsTotal', 'achievementsUnlocked',
|
||||
'achievementsRemaining', 'completionPercent', 'achievements', 'achievementSets', 'platforms', 'achievementsStale'];
|
||||
|
||||
export default function useGame(appid) {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [reloads, setReloads] = useState(0);
|
||||
// Counts the finished Steam refreshes, so a reader of the stored achievements can look again.
|
||||
const [refreshes, setRefreshes] = useState(0);
|
||||
|
||||
// Another game starts empty, already in this render. A reload of the same game keeps the old data, so the edit
|
||||
// cards keep what is typed.
|
||||
@@ -21,19 +27,33 @@ export default function useGame(appid) {
|
||||
const signal = nextSignal();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}`;
|
||||
(async () => {
|
||||
const {ok, status, data: json} = await request(
|
||||
`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${appid}`, {signal});
|
||||
const {ok, status, data: json} = await request(`${url}?refresh=false`, {signal});
|
||||
if (signal.aborted) return;
|
||||
setIsLoading(false);
|
||||
if (status === 401) return;
|
||||
if (status === 404) setError('GAME_DETAIL.NOT_FOUND');
|
||||
else if (!ok || !json) setError(status === 0 ? 'INFO.ERROR.NETWORK_FAILURE' : 'GAME_DETAIL.ERROR_LOAD');
|
||||
else setData(json);
|
||||
if (!ok || !json?.achievementsStale) return;
|
||||
// A failed refresh keeps the stored data, no error.
|
||||
const refreshed = await request(url, {signal});
|
||||
if (signal.aborted) return;
|
||||
if (refreshed.ok && refreshed.data) {
|
||||
const fresh = Object.fromEntries(ACHIEVEMENT_FIELDS.map((field) => [field, refreshed.data[field]]));
|
||||
// The finish rule may have set the status during the refresh; keep a status the user changed since.
|
||||
setData((d) => {
|
||||
if (!d) return refreshed.data;
|
||||
const unchanged = d.status === json.status;
|
||||
return {...d, ...fresh, ...(unchanged && {status: refreshed.data.status, finishedHint: refreshed.data.finishedHint})};
|
||||
});
|
||||
}
|
||||
setRefreshes((n) => n + 1);
|
||||
})();
|
||||
}, [nextSignal, appid, reloads]);
|
||||
|
||||
const reload = useCallback(() => setReloads((n) => n + 1), []);
|
||||
|
||||
return {data, setData, isLoading, error, reload};
|
||||
return {data, setData, isLoading, error, reload, refreshes};
|
||||
}
|
||||
@@ -15,7 +15,6 @@ vi.mock('../hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), b
|
||||
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
|
||||
const games = Array.from({length: 12}, (_, i) => ({appid: 100 + i, name: `Game ${i}`, playtimeMinutes: 1000 - i}));
|
||||
const empty = {content: [], total: 0, byStatus: {}};
|
||||
const mostPlayed = (url) => url.includes('sort=playtimeMinutes') && !url.includes('status=');
|
||||
const quietRoutes = {
|
||||
'GET /api/library/summary': json(200, empty),
|
||||
'GET /api/hints': json(200, {results: []}),
|
||||
@@ -27,7 +26,7 @@ const quietRoutes = {
|
||||
const dashboardRoutes = (settings) => ({
|
||||
...quietRoutes,
|
||||
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000', settings}),
|
||||
'GET /api/library/games': (url) => json(200, mostPlayed(url) ? {content: games, total: 12} : empty),
|
||||
'GET /api/library/dashboard-games': json(200, {top: games, playing: []}),
|
||||
'GET /api/library/stats': json(200, {trend: null, habits: {moonTrips: 1, longestDay: null, underOneHourGames: 0, underOneHourShare: null}}),
|
||||
'GET /api/library/steam-profile': json(200, {personaName: 'Lobo', avatarUrl: avatar}),
|
||||
'GET /api/library/activity': json(200, {totalMinutes: 90, days: [{day: '2026-09-23', minutes: 0}, {day: '2026-09-24', minutes: 90}], recentGames: [
|
||||
@@ -91,13 +90,23 @@ describe('Dashboard', () => {
|
||||
fetchRoutes({...routes, 'GET /api/users/me': () => meAnswer});
|
||||
const container = renderDashboard().container;
|
||||
|
||||
await waitFor(() => expect(asked('/api/library/games')).toBe(true));
|
||||
await waitFor(() => expect(asked('/api/library/dashboard-games')).toBe(true));
|
||||
expect(asked('/api/library/summary')).toBe(true);
|
||||
|
||||
answerMe(me);
|
||||
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||
});
|
||||
|
||||
it('asks both game tiles in one call', async () => {
|
||||
const container = mockDashboard();
|
||||
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||
|
||||
const gameCalls = fetch.mock.calls.map(([url]) => url).filter((url) => url.includes('dashboard-games'));
|
||||
expect(gameCalls).toHaveLength(1);
|
||||
expect(gameCalls[0]).toContain('top=10&playing=8');
|
||||
expect(asked('/api/library/games?')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows today\'s tiles without a saved layout', async () => {
|
||||
const container = mockDashboard();
|
||||
|
||||
@@ -231,9 +240,7 @@ describe('Dashboard', () => {
|
||||
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo'}),
|
||||
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}),
|
||||
'GET /api/library/backlog': json(200, {games: []}),
|
||||
'GET /api/library/games': (url) => (mostPlayed(url)
|
||||
? {ok: true, status: 200, json: async () => { throw new SyntaxError('Unexpected token <'); }}
|
||||
: json(200, empty)),
|
||||
'GET /api/library/dashboard-games': {ok: true, status: 200, json: async () => { throw new SyntaxError('Unexpected token <'); }},
|
||||
});
|
||||
renderDashboard();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('Dashboard sync on a visit', () => {
|
||||
'GET /api/news': json(200, {results: []}),
|
||||
'GET /api/library/sync-changes': () => syncChanges,
|
||||
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000'}),
|
||||
'GET /api/library/games': json(200, empty),
|
||||
'GET /api/library/dashboard-games': json(200, {top: [], playing: []}),
|
||||
'GET /api/library/stats': json(200, {trend: null}),
|
||||
'GET /api/library/steam-profile': json(200, {personaName: 'Lobo', avatarUrl: null}),
|
||||
'GET /api/library/activity': json(200, {totalMinutes: 0, days: [], recentGames: []}),
|
||||
@@ -49,7 +49,7 @@ describe('Dashboard sync on a visit', () => {
|
||||
syncStatus = {lastSyncStatus: 'RUNNING', lastSyncedAt: '2026-10-03T10:00:00Z'};
|
||||
await wait(3000);
|
||||
expect(screen.getByText('LIBRARY.SYNCING')).toBeTruthy();
|
||||
const gamesBefore = calls('/api/library/games');
|
||||
const gamesBefore = calls('/api/library/dashboard-games');
|
||||
const changesBefore = calls('/api/library/sync-changes');
|
||||
|
||||
syncStatus = {lastSyncStatus: 'OK', lastSyncedAt: '2026-10-04T08:00:00Z'};
|
||||
@@ -60,6 +60,6 @@ describe('Dashboard sync on a visit', () => {
|
||||
expect(screen.queryByText('LIBRARY.SYNCING')).toBeNull();
|
||||
expect(await screen.findByText('SYNC_CHANGES.ADDED:1')).toBeTruthy();
|
||||
expect(calls('/api/library/sync-changes')).toBeGreaterThan(changesBefore);
|
||||
expect(calls('/api/library/games')).toBeGreaterThan(gamesBefore);
|
||||
expect(calls('/api/library/dashboard-games')).toBeGreaterThan(gamesBefore);
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ export default function GameDetail() {
|
||||
const {appid} = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {data, setData, isLoading, error, reload} = useGame(appid);
|
||||
const {data, setData, isLoading, error, reload, refreshes} = useGame(appid);
|
||||
const [correcting, setCorrecting] = useState(false);
|
||||
const [merging, setMerging] = useState(false);
|
||||
const [linking, setLinking] = useState(false);
|
||||
@@ -96,7 +96,7 @@ export default function GameDetail() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<GamePointsLine key={`points-${data.appid}`} game={data}/>
|
||||
<GamePointsLine key={`points-${data.appid}`} game={data} refreshes={refreshes}/>
|
||||
|
||||
{/* The finished question also answers "are you playing it?". */}
|
||||
{!(data.finishedHint && ['BACKLOG', 'PLAYING'].includes(data.status)) && (
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, expect, it, vi} from 'vitest';
|
||||
import {cleanup, screen, waitFor} from '@testing-library/react';
|
||||
import GameDetail from './GameDetail.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import {renderWithProviders} from '../test/renderWithProviders.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: Object.assign(vi.fn(), {success: vi.fn(), error: vi.fn()})}));
|
||||
|
||||
const game = {
|
||||
appid: 220, name: 'Half-Life 2', playtimeMinutes: 100, playtime2Weeks: 0, source: 'STEAM', status: 'FINISHED',
|
||||
rating: null, notes: null, priority: null, hasAchievements: true, statsVisible: true, achievements: [],
|
||||
};
|
||||
|
||||
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('checksThePointsAgainAfterTheAchievementRefresh', async () => {
|
||||
let finishRefresh;
|
||||
const answers = [json(200, {...game, achievementsStale: true, achievementsUnlocked: 3}),
|
||||
new Promise((resolve) => { finishRefresh = resolve; })];
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {settings: {}}),
|
||||
'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: false, page: 0, totalPages: 1}),
|
||||
'GET /api/rewards/wallet': json(200, {balance: 0, catalog: [], owned: [], equipped: {}, history: []}),
|
||||
'GET /api/rewards/games/220': json(200, {balance: 0, missing: {FINISHED: 'NO_PROOF'}, paid: []}),
|
||||
'GET /api/library/games/220': () => answers.shift(),
|
||||
});
|
||||
const pointsCalls = () => fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/games/220')).length;
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
await screen.findByText('Half-Life 2');
|
||||
await waitFor(() => expect(pointsCalls()).toBe(1));
|
||||
|
||||
// The refresh stored the ending achievement: Core would settle the finish now (the old single call did).
|
||||
finishRefresh(json(200, {...game, achievementsStale: false, achievementsUnlocked: 5}));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
expect(pointsCalls()).toBe(2);
|
||||
});
|
||||
@@ -17,15 +17,17 @@ const game = {
|
||||
rating: null, notes: null, priority: null, hasAchievements: false, statsVisible: false, achievements: [],
|
||||
};
|
||||
|
||||
const gameLoads = () => fetch.mock.calls.filter(([url]) => url.endsWith('/api/library/games/220')).length;
|
||||
const gameUrls = () => fetch.mock.calls.map(([url]) => url).filter((url) => new URL(url, 'http://x').pathname.endsWith('/api/library/games/220'));
|
||||
const gameLoads = () => gameUrls().length;
|
||||
const gameRoutes = (gameAnswer) => fetchRoutes({
|
||||
'GET /api/users/me': json(200, {role: 'AUDITOR', activeRole: 'AUDITOR'}),
|
||||
'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: false, page: 0, totalPages: 1}),
|
||||
'GET /api/library/games/220': gameAnswer,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {role: 'AUDITOR', activeRole: 'AUDITOR'}),
|
||||
'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: false, page: 0, totalPages: 1}),
|
||||
'GET /api/library/games/220': json(200, game),
|
||||
});
|
||||
gameRoutes(json(200, game));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -42,3 +44,74 @@ it('keeps unsaved notes while the game loads again after a correction', async ()
|
||||
await waitFor(() => expect(gameLoads()).toBe(2));
|
||||
await waitFor(() => expect(screen.getByLabelText('BACKLOG.NOTES_LABEL').value).toBe('my draft'));
|
||||
});
|
||||
|
||||
it('shows the stored game, then the refreshed one when the achievements are stale', async () => {
|
||||
const answers = [json(200, {...game, achievementsStale: true}),
|
||||
json(200, {...game, hasAchievements: true, statsVisible: true, achievementsTotal: 1, achievementsUnlocked: 1,
|
||||
achievements: [{apiName: 'A', displayName: 'Refreshed achievement', unlocked: true, kind: 'STEAM'}],
|
||||
achievementSets: [{id: 'steam', kind: 'STEAM', total: 1, unlocked: 1, perfect: true, visible: true, listed: true}],
|
||||
achievementsStale: false})];
|
||||
gameRoutes(() => answers.shift());
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
expect(await screen.findByText('Refreshed achievement')).toBeTruthy();
|
||||
expect(gameUrls()).toHaveLength(2);
|
||||
expect(gameUrls()[0]).toContain('refresh=false');
|
||||
expect(gameUrls()[1]).not.toContain('refresh');
|
||||
});
|
||||
|
||||
it('keeps the stored game when the refreshing call fails', async () => {
|
||||
const answers = [json(200, {...game, achievementsStale: true}), json(500, {})];
|
||||
gameRoutes(() => answers.shift());
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
expect(await screen.findByText('Half-Life 2')).toBeTruthy();
|
||||
await waitFor(() => expect(gameUrls()).toHaveLength(2));
|
||||
expect(screen.getByText('Half-Life 2')).toBeTruthy();
|
||||
expect(screen.queryByText('GAME_DETAIL.ERROR_LOAD')).toBeNull();
|
||||
});
|
||||
|
||||
it('asks once when the achievements are fresh', async () => {
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
expect(await screen.findByText('Half-Life 2')).toBeTruthy();
|
||||
expect(gameUrls()).toHaveLength(1);
|
||||
expect(gameUrls()[0]).toContain('refresh=false');
|
||||
});
|
||||
|
||||
// The refreshing call answers the status read before a save made in its wait; only its achievements are taken.
|
||||
it('keepsAStatusSavedWhileTheAchievementsRefresh', async () => {
|
||||
let finishRefresh;
|
||||
const answers = [json(200, {...game, achievementsStale: true}),
|
||||
new Promise((resolve) => { finishRefresh = resolve; })];
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {role: 'AUDITOR', activeRole: 'AUDITOR'}),
|
||||
'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: false, page: 0, totalPages: 1}),
|
||||
'GET /api/library/games/220': () => answers.shift(),
|
||||
'PUT /api/library/games/220/status': (url, init) => json(200, {...game, ...JSON.parse(init.body)}),
|
||||
});
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'}));
|
||||
await waitFor(() => expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull());
|
||||
|
||||
// Core read the game (BACKLOG) before the save, then waited for Steam.
|
||||
finishRefresh(json(200, {...game, achievementsStale: false}));
|
||||
await waitFor(() => expect(gameLoads()).toBe(2));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull();
|
||||
});
|
||||
|
||||
// The finish rule (ending achievement unlocked) sets the status during the refresh.
|
||||
it('showsTheStatusTheRefreshSetWhenTheUserChangedNothing', async () => {
|
||||
let finishRefresh;
|
||||
const answers = [json(200, {...game, achievementsStale: true}),
|
||||
new Promise((resolve) => { finishRefresh = resolve; })];
|
||||
gameRoutes(() => answers.shift());
|
||||
renderWithProviders(<GameDetail/>, {path: '/library/:appid', entries: ['/library/220']});
|
||||
|
||||
expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeTruthy();
|
||||
finishRefresh(json(200, {...game, status: 'FINISHED', achievementsStale: false}));
|
||||
await waitFor(() => expect(gameLoads()).toBe(2));
|
||||
await waitFor(() => expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull());
|
||||
});
|
||||
@@ -18,7 +18,7 @@ vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
// The last column: what the page loads besides GET /api/users/me of the provider.
|
||||
const pages = [
|
||||
['Dashboard', '/', '/', Dashboard, 'INFO.ERROR.LOAD_PROFILE',
|
||||
['/api/library/games', '/api/library/games-count', '/api/library/backlog', '/api/library/summary',
|
||||
['/api/library/dashboard-games', '/api/library/games-count', '/api/library/backlog', '/api/library/summary',
|
||||
'/api/library/activity', '/api/library/achievements/summary']],
|
||||
['GameDetail', '/games/:appid', '/games/220', GameDetail, 'GAME_DETAIL.ERROR_LOAD', ['/api/library/games/220']],
|
||||
['GameLibrary', '/library', '/library', GameLibrary, 'LIBRARY.ERROR_LOAD',
|
||||
|
||||
Reference in new issue
Block a user