// @vitest-environment jsdom import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {cleanup, fireEvent, screen, waitFor} from '@testing-library/react'; import {toast} from 'sonner'; import ManualGameCard from './ManualGameCard.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: {success: vi.fn(), error: vi.fn()}})); const game = {appid: -1025, platform: 'PS5', playtimeMinutes: 90}; const answer = (routes) => fetchRoutes({'GET /api/users/me': json(200, {settings: {}}), ...routes}); const renderCard = (onSaved = vi.fn()) => renderWithProviders(, {path: '/library/:appid', entries: ['/library/-1025'], routes: {'/library':

library page

}}); const saveButton = () => screen.getByRole('button', {name: 'BACKLOG.SAVE'}); beforeEach(() => { localStorage.setItem('tbp_session', '1'); }); afterEach(() => { cleanup(); localStorage.clear(); vi.clearAllMocks(); }); describe('ManualGameCard', () => { it('saves the trimmed platform and the minutes, then reports them', async () => { answer({'PUT /api/library/games/-1025/manual': json(200, {})}); const onSaved = vi.fn(); renderCard(onSaved); fireEvent.change(screen.getByLabelText('MANUAL_GAME.PLATFORM'), {target: {value: ' Switch '}}); fireEvent.change(screen.getByLabelText('MANUAL_GAME.HOURS'), {target: {value: '2'}}); fireEvent.click(saveButton()); await waitFor(() => expect(onSaved).toHaveBeenCalledWith({platform: 'Switch', playtimeMinutes: 120})); 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: 'Switch', playtimeMinutes: 120}); }); it('keeps the save button disabled while nothing changed', () => { answer({}); renderCard(); expect(saveButton().disabled).toBe(true); fireEvent.change(screen.getByLabelText('MANUAL_GAME.PLATFORM'), {target: {value: 'PS5 '}}); expect(saveButton().disabled).toBe(true); }); it('removes the game only after a confirm and opens the library', async () => { answer({'DELETE /api/library/games/-1025': json(204, null)}); const {router} = renderCard(); fireEvent.click(screen.getByRole('button', {name: 'MANUAL_GAME.REMOVE'})); expect(fetch.mock.calls.some(([, init]) => init?.method === 'DELETE')).toBe(false); fireEvent.click(screen.getByRole('button', {name: 'MANUAL_GAME.REMOVE_YES'})); await waitFor(() => expect(router.state.location.pathname).toBe('/library')); }); it('shows an error when the save fails', async () => { answer({'PUT /api/library/games/-1025/manual': json(500, {})}); const onSaved = vi.fn(); renderCard(onSaved); fireEvent.change(screen.getByLabelText('MANUAL_GAME.PLATFORM'), {target: {value: 'Switch'}}); fireEvent.click(saveButton()); await waitFor(() => expect(toast.error).toHaveBeenCalledWith('BACKLOG.SAVE_FAILED')); expect(onSaved).not.toHaveBeenCalled(); }); it('shows no toast on a 401', async () => { answer({'PUT /api/library/games/-1025/manual': json(401, {}), 'POST /api/auth/refresh': json(401, {})}); const onSaved = vi.fn(); renderCard(onSaved); fireEvent.change(screen.getByLabelText('MANUAL_GAME.PLATFORM'), {target: {value: 'Switch'}}); fireEvent.click(saveButton()); await waitFor(() => expect(fetch.mock.calls.some(([url]) => url.endsWith('/api/auth/refresh'))).toBe(true)); await waitFor(() => expect(saveButton().disabled).toBe(false)); expect(toast.error).not.toHaveBeenCalled(); expect(toast.success).not.toHaveBeenCalled(); expect(onSaved).not.toHaveBeenCalled(); }); });