fix: abort old loads, stale sync state, warn before unsaved edits are lost

This commit is contained in:
lobo committed 2026-09-30 07:51:09 +02:00
1 parent cbafa3d540
commit 9ddc50f819
28 files changed
+753 -121

No files matched your search

+51
View File
@@ -6,8 +6,11 @@ import GameLibrary from './GameLibrary.jsx';
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
import {CurrentUserProvider} from '../context/CurrentUserContext.jsx';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
import {renderWithProviders} from '../test/renderWithProviders.jsx';
import {toast} from 'sonner';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
vi.mock('../hooks/useLibrarySync.js', () => ({default: () => ({status: 'OK', lastSyncedAt: null, isRunning: false})}));
vi.mock('../hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), busy: false})}));
@@ -70,6 +73,36 @@ describe('GameLibrary search', () => {
expect(screen.queryByText(/\/5$/)).toBeNull();
});
it('shows the newest search when an older answer comes last', async () => {
const later = [];
const games = (name) => json(200, {...page, content: [{...page.content[0], name}], totalElements: 1, totalPages: 1});
fetchRoutes({
'GET /api/users/me': json(200, {settings: {}}),
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}),
'GET /api/library/games': (url, init) => {
const q = new URL(url, 'http://x').searchParams.get('q');
if (!q) return games('Half-Life 2');
// The first search answers only after the second one.
return new Promise((resolve, reject) => {
later.push({q, answer: () => resolve(games(q === 'ha' ? 'Old answer' : 'New answer'))});
init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
});
},
});
renderLibrary();
await screen.findByText('Half-Life 2');
typeInto('ha');
await waitFor(() => expect(later).toHaveLength(1));
typeInto('half');
await waitFor(() => expect(later).toHaveLength(2));
await act(async () => later[1].answer());
await act(async () => later[0].answer());
expect(screen.getByText('New answer')).toBeTruthy();
expect(screen.queryByText('Old answer')).toBeNull();
});
it('writes the trimmed search to the URL after the delay and resets the page', async () => {
const lib = renderLibrary('/library?page=3');
@@ -216,3 +249,21 @@ describe('GameLibrary with games added by hand', () => {
expect(screen.getByText('LIBRARY.RESYNC')).toBeTruthy();
});
});
describe('GameLibrary session', () => {
it('opens the login page on a 401 that the refresh cannot fix, without an error', async () => {
fetchRoutes({
'GET /api/users/me': json(200, {settings: {}}),
'GET /api/library/games': json(401, {}),
'GET /api/library/games-count': json(401, {}),
'POST /api/auth/refresh': json(401, {}),
});
const {router} = renderWithProviders(<GameLibrary/>, {path: '/library', entries: ['/library?q=zelda'],
guarded: true, routes: {'/login': <p>login</p>}});
await waitFor(() => expect(router.state.location.pathname).toBe('/login'));
expect(router.state.location.state).toEqual({sessionExpired: true, from: '/library?q=zelda'});
expect(toast.error).not.toHaveBeenCalled();
expect(screen.queryByText('LIBRARY.ERROR_LOAD')).toBeNull();
});
});