// @vitest-environment jsdom import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {act, cleanup, fireEvent, screen, waitFor} from '@testing-library/react'; import GameDetail from './GameDetail.jsx'; import {fetchRoutes, json} from '../test/fetchRoutes.js'; import {renderWithProviders} from '../test/renderWithProviders.jsx'; import {resetBrokenImageReports} from '../utils/brokenImages.js'; import {toast} from 'sonner'; vi.mock('react-i18next', () => import('../test/i18n-mock.js')); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}})); const game = { appid: 220, name: 'Half-Life 2', playtimeMinutes: 100, playtime2Weeks: 90, status: 'BACKLOG', rating: null, notes: null, priority: null, hasAchievements: false, statsVisible: false, achievements: [], }; const renderDetail = (appid = 220) => renderWithProviders(, {path: '/library/:appid', entries: [`/library/${appid}`], routes: {'/library':

library page

}}); const echo = (url, init) => json(200, JSON.parse(init.body)); // The user and the game at /library/; `routes` adds or replaces answers. const answer = (shown, routes = {}, me = {settings: {}}) => fetchRoutes({ 'GET /api/users/me': json(200, me), 'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: false, page: 0, totalPages: 1}), [`GET /api/library/games/${shown.appid}`]: json(200, shown), ...routes, }); beforeEach(() => { localStorage.setItem('tbp_session', '1'); }); afterEach(() => { cleanup(); localStorage.clear(); resetBrokenImageReports(); }); describe('GameDetail', () => { it('showsHowManyOthersOwnTheGameAndOnlyNamesTheListedOnes', async () => { answer(game, {'GET /api/stats/owners': json(200, {listed: [{displayName: 'Lobo'}], listedTotal: 1, privateCount: 1, includesYou: true, page: 0, totalPages: 1})}); renderDetail(); expect(await screen.findByText('PEOPLE.OTHER_OWNERS:2')).toBeTruthy(); expect(screen.getByRole('link', {name: 'Lobo'}).getAttribute('href')).toBe('/u/Lobo'); expect(screen.getByText('PEOPLE.PRIVATE:1')).toBeTruthy(); expect(screen.queryByText('PEOPLE.YOU')).toBeNull(); expect(fetch.mock.calls.find(([url]) => url.includes('/api/stats/owners'))[0]).toContain('appid=220&page=0'); }); it('showsNoOwnersTitleWhenNobodyElseOwnsTheGame', async () => { answer(game, {'GET /api/stats/owners': json(200, {listed: [], listedTotal: 0, privateCount: 0, includesYou: true, page: 0, totalPages: 0})}); renderDetail(); expect(await screen.findByText('PEOPLE.NONE')).toBeTruthy(); expect(screen.queryByText(/PEOPLE.OTHER_OWNERS/)).toBeNull(); }); it('shows the coming badge of a game that is not released yet', async () => { answer({...game, source: 'STEAM', coming: true, comingOn: '2026-12-04'}); renderDetail(); expect(await screen.findByText('LIBRARY.COMING')).toBeTruthy(); }); it('shows no coming badge for a released game', async () => { answer({...game, source: 'STEAM', coming: false, comingOn: null}); renderDetail(); await screen.findByText('Half-Life 2'); expect(screen.queryByText('LIBRARY.COMING')).toBeNull(); expect(screen.queryByText('LIBRARY.COMING_SOON')).toBeNull(); }); it('asks "finished?" instead of "playing?" when Core has a finished hint', async () => { answer({...game, playtime2Weeks: 90, source: 'STEAM', finishedHint: {type: 'ENDING', achievement: 'Survivor'}}); renderDetail(); expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_FINISHED'})).toBeTruthy(); expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull(); }); it('hides a Steam game and shows it again', async () => { answer({...game, source: 'STEAM', hidden: false}, {'PUT /api/library/games/220/hidden': echo}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.HIDE'})); expect(await screen.findByText('GAME_DETAIL.HIDDEN_NOTE')).toBeTruthy(); const [url, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT'); expect(url).toContain('/api/library/games/220/hidden'); expect(JSON.parse(init.body)).toEqual({hidden: true}); fireEvent.click(screen.getByRole('button', {name: 'GAME_DETAIL.UNHIDE'})); await waitFor(() => expect(screen.queryByText('GAME_DETAIL.HIDDEN_NOTE')).toBeNull()); expect(screen.getByRole('button', {name: 'GAME_DETAIL.HIDE'})).toBeTruthy(); }); it('keeps the game shown when Core refuses', async () => { answer({...game, source: 'STEAM', hidden: false}, {'PUT /api/library/games/220/hidden': json(500, {})}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.HIDE'})); await waitFor(() => expect(toast.error).toHaveBeenCalledWith('GAME_DETAIL.HIDE_ERROR')); expect(screen.queryByText('GAME_DETAIL.HIDDEN_NOTE')).toBeNull(); }); it('hides a game added by hand and shows it again', async () => { answer({...game, appid: -1942, source: 'MANUAL', platform: 'PS5', hidden: false}, {'PUT /api/library/games/-1942/hidden': echo}); renderDetail(-1942); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.HIDE'})); expect(await screen.findByText('GAME_DETAIL.HIDDEN_NOTE')).toBeTruthy(); expect(screen.queryByText('GAME_DETAIL.OPEN_IN_STEAM')).toBeNull(); fireEvent.click(screen.getByRole('button', {name: 'GAME_DETAIL.UNHIDE'})); await waitFor(() => expect(screen.queryByText('GAME_DETAIL.HIDDEN_NOTE')).toBeNull()); }); it('reports a broken achievement icon once and shows the empty tile', async () => { const achievement = (apiName) => ({apiName, displayName: apiName, unlocked: false, iconUrl: `https://x/${apiName}.jpg`, iconGrayUrl: `https://x/${apiName}_gray.jpg`, kind: 'STEAM', setId: null}); answer({...game, hasAchievements: true, statsVisible: true, achievementsTotal: 2, achievementsUnlocked: 0, completionPercent: 0, achievements: [achievement('A'), achievement('B')], achievementSets: [{id: 'steam', kind: 'STEAM', total: 2, unlocked: 0, perfect: false, visible: true, listed: true}]}, {'POST /api/library/broken-images': json(204)}); const {container} = renderDetail(); await screen.findByText('A'); container.querySelectorAll('img[src^="https://x/"]').forEach((img) => fireEvent.error(img)); const reports = fetch.mock.calls.filter(([, init]) => init?.method === 'POST'); expect(reports).toHaveLength(1); expect(reports[0][0]).toContain('/api/library/broken-images'); expect(JSON.parse(reports[0][1].body)).toEqual({appid: 220, kind: 'ACHIEVEMENT_ICONS'}); expect(container.querySelector('img[src^="https://x/"]')).toBeNull(); }); it('shows the trophies of a PlayStation game and not the no-achievements card', async () => { answer({...game, source: 'PSN', hasAchievements: true, statsVisible: true, achievements: [{apiName: '0', displayName: 'Legend', unlocked: true, kind: 'PSN', setId: 'NPWR1_00', trophyType: 'PLATINUM', iconUrl: 'https://psnobj.prod.dl.playstation.net/0.png'}], achievementSets: [{id: 'NPWR1_00', kind: 'PSN', platform: 'PS5', name: 'Ghost', total: 1, unlocked: 1, perfect: true, visible: true, listed: true}]}); renderDetail(); expect(await screen.findByText('Legend')).toBeTruthy(); expect(screen.getByText('GAME_DETAIL.TROPHIES')).toBeTruthy(); expect(screen.queryByText('GAME_DETAIL.NO_ACHIEVEMENTS')).toBeNull(); }); it('links the speedrun.com page when Core found one', async () => { answer({...game, speedrunUrl: 'https://www.speedrun.com/hl2'}); renderDetail(); const link = await screen.findByRole('link', {name: /GAME_DETAIL.OPEN_IN_SPEEDRUN/}); expect(link.getAttribute('href')).toBe('https://www.speedrun.com/hl2'); }); it.each([null, 'https://www.speedrun.com.evil.example/hl2', 'https://evil.example/hl2', 42])( 'shows no speedrun link for %s', async (speedrunUrl) => { answer({...game, speedrunUrl}); renderDetail(); await screen.findByText('GAME_DETAIL.OPEN_IN_STEAM'); expect(screen.queryByText('GAME_DETAIL.OPEN_IN_SPEEDRUN')).toBeNull(); }); it('keeps unsaved notes in the backlog card when the hint sets the game to playing', async () => { // Core changes only the status and answers the whole entry. answer(game, {'PUT /api/library/games/220/status': (url, init) => json(200, {...game, ...JSON.parse(init.body)})}); renderDetail(); const notes = await screen.findByLabelText('BACKLOG.NOTES_LABEL'); fireEvent.change(notes, {target: {value: 'my draft'}}); fireEvent.click(screen.getByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})); await waitFor(() => expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull()); expect(screen.getByLabelText('BACKLOG.NOTES_LABEL').value).toBe('my draft'); }); it('uses the game as background and can remove it again', async () => { answer(game, {'PUT /api/users/settings/appearance': echo}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})); const remove = await screen.findByRole('button', {name: 'GAME_DETAIL.REMOVE_BACKGROUND'}); const [url, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT'); expect(url).toContain('/api/users/settings/appearance'); expect(JSON.parse(init.body).backgroundAppid).toBe(220); fireEvent.click(remove); expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})).toBeTruthy(); }); it('uses the game as banner and goes back to the automatic banner', async () => { answer(game, {'PUT /api/users/settings/appearance': echo}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'})); const auto = await screen.findByRole('button', {name: 'SETTINGS.BANNER_AUTO'}); const [, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT'); expect(JSON.parse(init.body)).toMatchObject({bannerAppid: 220, showBanner: true}); fireEvent.click(auto); expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'})).toBeTruthy(); }); it('offers "use as banner" for the picked game while the banner is off, and turns it on', async () => { localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 220, showBanner: false})); answer(game, {'PUT /api/users/settings/appearance': echo}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'})); await waitFor(() => expect(fetch.mock.calls.some(([, call]) => call?.method === 'PUT')).toBe(true)); const [, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT'); expect(JSON.parse(init.body)).toMatchObject({bannerAppid: 220, showBanner: true}); }); }); describe('GameDetail of a game added by hand', () => { const manualGame = { ...game, appid: -1025, name: 'Zelda', source: 'MANUAL', platform: 'Nintendo Switch', playtimeMinutes: 90, playtime2Weeks: 0, imageUrl: 'https://images.igdb.com/z.jpg', }; const noBody = {ok: true, status: 204, json: async () => { throw new Error('204 has no body'); }}; beforeEach(() => { answer(manualGame, {'PUT /api/library/games/-1025/manual': noBody, 'DELETE /api/library/games/-1025': noBody}); }); it('shows platform and IGDB picture, and offers no Steam actions', async () => { const {container} = renderDetail(-1025); expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy(); expect(screen.getAllByText('Nintendo Switch').length).toBeGreaterThan(0); expect(container.querySelector('img[src="https://images.igdb.com/z.jpg"]')).toBeTruthy(); expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})).toBeNull(); expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_BANNER'})).toBeNull(); expect(screen.queryByText('GAME_DETAIL.OPEN_IN_STEAM')).toBeNull(); expect(screen.getByText('MANUAL_GAME.NO_ACHIEVEMENTS')).toBeTruthy(); }); it('treats a game from a PlayStation import like a game added by hand', async () => { answer({...manualGame, source: 'PSN', platform: 'PS5'}, {'PUT /api/library/games/-1025/manual': noBody, 'DELETE /api/library/games/-1025': noBody}); renderDetail(-1025); expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy(); expect(screen.getByText('MANUAL_GAME.NO_ACHIEVEMENTS')).toBeTruthy(); }); it('psnRowOffersNoHoursField', async () => { answer({...manualGame, source: 'PSN', platform: 'PS5'}); renderDetail(-1025); expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy(); expect(screen.queryByLabelText('MANUAL_GAME.HOURS')).toBeNull(); }); it('shows when a PlayStation game was last played, and nothing for a game without that date', async () => { answer({...manualGame, source: 'PSN', platform: 'PS5', lastPlayedAt: '2026-10-05T18:30:00Z'}, {'PUT /api/library/games/-1025/manual': noBody, 'DELETE /api/library/games/-1025': noBody}); renderDetail(-1025); expect(await screen.findByText('GAME_DETAIL.LAST_PLAYED')).toBeTruthy(); cleanup(); answer({...manualGame, source: 'PSN', platform: 'PS5', lastPlayedAt: null}); renderDetail(-1025); expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy(); expect(screen.queryByText('GAME_DETAIL.LAST_PLAYED')).toBeNull(); }); it('saves platform and hours', async () => { renderDetail(-1025); fireEvent.change(await screen.findByLabelText('MANUAL_GAME.HOURS'), {target: {value: '2.5'}}); fireEvent.change(screen.getByLabelText('MANUAL_GAME.PLATFORM'), {target: {value: ' PS5 '}}); const saveButtons = screen.getAllByRole('button', {name: 'BACKLOG.SAVE'}); fireEvent.click(saveButtons.at(-1)); await waitFor(() => expect(fetch.mock.calls.some(([, call]) => call?.method === 'PUT')).toBe(true)); const [url, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT'); expect(url).toContain('/api/library/games/-1025/manual'); expect(JSON.parse(init.body)).toEqual({platform: 'PS5', playtimeMinutes: 150}); }); it('removes the game only after the confirm step and goes back to the library', async () => { renderDetail(-1025); fireEvent.click(await screen.findByRole('button', {name: 'MANUAL_GAME.REMOVE'})); expect(fetch.mock.calls.some(([, call]) => call?.method === 'DELETE')).toBe(false); fireEvent.click(screen.getByRole('button', {name: 'MANUAL_GAME.REMOVE_YES'})); expect(await screen.findByText('library page')).toBeTruthy(); const [url] = fetch.mock.calls.find(([, call]) => call?.method === 'DELETE'); expect(url).toContain('/api/library/games/-1025'); }); it('keeps Save off for an unchanged game with 75 minutes', async () => { answer({...manualGame, playtimeMinutes: 75}); renderDetail(-1025); await screen.findByLabelText('MANUAL_GAME.HOURS'); expect(screen.getAllByRole('button', {name: 'BACKLOG.SAVE'}).at(-1).disabled).toBe(true); }); it('shows no manual card for a Steam game', async () => { answer({...game, source: 'STEAM'}); renderDetail(); expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})).toBeTruthy(); expect(screen.queryByText('MANUAL_GAME.TITLE')).toBeNull(); }); it('keeps a background set in another tab when it sets the profile banner', async () => { let backgroundAppid = null; answer({...game, source: 'STEAM'}, { 'GET /api/users/me': () => json(200, {role: 'USER', settings: {profileStyle: {accent: 'red', corners: 'round', backgroundAppid}}}), 'PUT /api/users/settings/profile-style': json(204), }); renderDetail(); const menu = await screen.findByRole('button', {name: 'GAME_DETAIL.PROFILE_ART'}); backgroundAppid = 620; // set in a second tab fireEvent.keyDown(menu, {key: 'Enter'}); fireEvent.click(await screen.findByRole('menuitemcheckbox', {name: 'GAME_DETAIL.PROFILE_BANNER'})); await waitFor(() => expect(fetch.mock.calls.some(([u, init]) => init?.method === 'PUT' && u.endsWith('/profile-style'))).toBe(true)); const sent = fetch.mock.calls.filter(([u, init]) => init?.method === 'PUT' && u.endsWith('/profile-style')) .map(([, init]) => JSON.parse(init.body)); expect(sent).toEqual([{accent: 'red', corners: 'round', bannerAppid: 220, backgroundAppid: 620}]); }); it('keeps a banner picked in another tab when it sets this game as banner', async () => { let bannerAppid = 220; answer({...game, source: 'STEAM'}, { 'GET /api/users/me': () => json(200, {role: 'USER', settings: {profileStyle: {bannerAppid}}}), 'PUT /api/users/settings/profile-style': json(204), }); renderDetail(); const menu = await screen.findByRole('button', {name: 'GAME_DETAIL.PROFILE_ART'}); bannerAppid = 620; // another tab picked another game fireEvent.keyDown(menu, {key: 'Enter'}); fireEvent.click(await screen.findByRole('menuitemcheckbox', {name: 'GAME_DETAIL.PROFILE_BANNER'})); await waitFor(() => expect(fetch.mock.calls.some(([u, init]) => init?.method === 'PUT' && u.endsWith('/profile-style'))).toBe(true)); const [, init] = fetch.mock.calls.find(([u, i]) => i?.method === 'PUT' && u.endsWith('/profile-style')); expect(JSON.parse(init.body).bannerAppid).toBe(220); }); it('uses the art as profile banner and removes it on the second click', async () => { let stored = {accent: 'red', corners: 'round'}; answer({...game, source: 'STEAM'}, { 'GET /api/users/me': () => json(200, {role: 'USER', settings: {profileStyle: stored}}), 'PUT /api/users/settings/profile-style': (url, init) => { stored = JSON.parse(init.body); return json(204); }, }); const styleBodies = () => fetch.mock.calls.filter(([url, init]) => init?.method === 'PUT' && url.endsWith('/profile-style')) .map(([, init]) => JSON.parse(init.body)); const openMenu = async () => fireEvent.keyDown(await screen.findByRole('button', {name: 'GAME_DETAIL.PROFILE_ART'}), {key: 'Enter'}); renderDetail(); await openMenu(); fireEvent.click(await screen.findByRole('menuitemcheckbox', {name: 'GAME_DETAIL.PROFILE_BANNER'})); await waitFor(() => expect(styleBodies()).toEqual([{accent: 'red', corners: 'round', bannerAppid: 220, backgroundAppid: null}])); await openMenu(); const banner = await screen.findByRole('menuitemcheckbox', {name: 'GAME_DETAIL.PROFILE_BANNER'}); expect(banner.getAttribute('aria-checked')).toBe('true'); fireEvent.click(banner); await waitFor(() => expect(styleBodies()).toHaveLength(2)); expect(styleBodies()[1].bannerAppid).toBeNull(); }); describe('corrections', () => { const loggedInAs = (me) => answer(game, {}, me); it('offers "correct data" to an active auditor', async () => { loggedInAs({role: 'AUDITOR', activeRole: 'AUDITOR'}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'CORRECTION.OPEN'})); expect(await screen.findByText('CORRECTION.TITLE')).toBeTruthy(); }); it('offers "merge duplicate" to an active auditor', async () => { loggedInAs({role: 'AUDITOR', activeRole: 'AUDITOR'}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'MERGE.OPEN'})); expect(await screen.findByText('MERGE.TITLE')).toBeTruthy(); }); it('showsTheOtherEditionsAndOffersUnlinkOnlyToAnAuditorOnALinkedGame', async () => { const linked = {...game, linked: true, editions: [{appid: -7, name: 'Half-Life 2 PS5', platform: 'PS5', source: 'MANUAL', status: 'FINISHED'}]}; answer(linked, {'DELETE /api/library/games/220/link': json(204)}, {role: 'AUDITOR', activeRole: 'AUDITOR'}); renderDetail(); expect(await screen.findByRole('link', {name: 'Half-Life 2 PS5'})).toBeTruthy(); fireEvent.click(await screen.findByRole('button', {name: 'LINKS.UNLINK'})); await waitFor(() => expect(fetch.mock.calls.some(([, init]) => init?.method === 'DELETE')).toBe(true)); expect(toast.success).toHaveBeenCalledWith('LINKS.UNLINKED'); }); it('offersNoUnlinkWithoutALink', async () => { loggedInAs({role: 'AUDITOR', activeRole: 'AUDITOR'}); renderDetail(); await screen.findByRole('button', {name: 'LINKS.LINK_WITH'}); expect(screen.queryByRole('button', {name: 'LINKS.UNLINK'})).toBeNull(); }); it('offers nothing to a user or to an admin viewing as user', async () => { for (const me of [{role: 'USER', activeRole: 'USER'}, {role: 'PLATFORM_ADMIN', activeRole: 'USER'}]) { loggedInAs(me); renderDetail(); await screen.findByText('Half-Life 2'); await waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/users/me'), expect.anything())); // Let the answer of /me reach the page before looking for the button. await act(async () => {}); expect(screen.queryByRole('button', {name: 'CORRECTION.OPEN'})).toBeNull(); expect(screen.queryByRole('button', {name: 'MERGE.OPEN'})).toBeNull(); expect(screen.queryByRole('button', {name: 'LINKS.UNLINK'})).toBeNull(); cleanup(); } }); }); }); describe('GameDetail edition requests', () => { it('opensTheSameGameDialogForAUser', async () => { answer(game, {}, {role: 'USER', activeRole: 'USER'}); renderDetail(); fireEvent.click(await screen.findByRole('button', {name: 'LINKS.SAME_AS'})); expect(await screen.findByText('LINKS.DIALOG_HINT')).toBeTruthy(); }); }); describe('GameDetail confetti', () => { // jsdom has no canvas drawing. beforeEach(() => { vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null); }); afterEach(() => { vi.restoreAllMocks(); }); const opened = async (entry) => { answer({...game, ...entry}); const {container} = renderDetail(); await screen.findByText('Half-Life 2'); return container.querySelector('canvas[data-size]'); }; it('plays a small one for a finished game', async () => { expect((await opened({status: 'FINISHED'})).dataset.size).toBe('small'); }); it('plays a big one when every achievement is unlocked too', async () => { const canvas = await opened({status: 'COMPLETED', hasAchievements: true, statsVisible: true, achievementsTotal: 3, achievementsUnlocked: 3}); expect(canvas.dataset.size).toBe('big'); }); it('plays nothing for a game not finished', async () => { expect(await opened({status: 'PLAYING'})).toBeNull(); }); }); describe('GameDetail session', () => { it('opens the login page on a 401 that the refresh cannot fix, without an error', async () => { toast.error.mockClear(); fetchRoutes({ 'GET /api/users/me': json(200, {settings: {}}), 'GET /api/library/games/220': json(401, {}), 'POST /api/auth/refresh': json(401, {}), }); const {router} = renderWithProviders(, {path: '/library/:appid', entries: ['/library/220'], guarded: true, routes: {'/login':

login

}}); await waitFor(() => expect(router.state.location.pathname).toBe('/login')); expect(router.state.location.state).toEqual({sessionExpired: true, from: '/library/220'}); expect(toast.error).not.toHaveBeenCalled(); expect(screen.queryByText('GAME_DETAIL.ERROR_LOAD')).toBeNull(); }); it('replacesTheUrlOfAMergedKeyWithTheKeptGame', async () => { 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/library/games/-7': json(200, {...game, appid: 220, source: 'STEAM'}), 'GET /api/library/games/220': json(200, {...game, appid: 220, source: 'STEAM'}), }); const {router} = renderDetail(-7); await waitFor(() => expect(router.state.location.pathname).toBe('/library/220')); expect(router.state.historyAction).toBe('REPLACE'); }); });