Public Access
440 lines
21 KiB
React
440 lines
21 KiB
React
// @vitest-environment jsdom
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {act, cleanup, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
|
|
import {MemoryRouter, Route, Routes, useLocation} from 'react-router-dom';
|
|
import {toast} from 'sonner';
|
|
import ImportPage from './ImportPage.jsx';
|
|
import {initialChoice, stepOf} from '../utils/imports.js';
|
|
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';
|
|
|
|
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
|
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn(), info: vi.fn()}}));
|
|
|
|
|
|
const hades = {igdbId: 1, name: 'Hades', year: 2020, imageUrl: null, platforms: ['PC', 'Switch'], inLibrary: false};
|
|
const hades2 = {igdbId: 2, name: 'Hades II', year: 2025, imageUrl: null, platforms: ['PC'], inLibrary: false};
|
|
const celeste = {igdbId: 3, name: 'Celeste', year: 2018, imageUrl: null, platforms: ['PC', 'PS4'], inLibrary: true};
|
|
const row = (lineNo, title, candidates, extra = {}) => ({
|
|
lineNo, title, platform: null, playtimeMinutes: null, status: null, searchFailed: false, exact: true, candidates, ...extra,
|
|
});
|
|
const ready = {
|
|
id: 5, source: 'TEXT', status: 'READY', total: 3, matched: 3, rows: [
|
|
row(1, 'Hades', [hades, hades2]),
|
|
row(2, 'Celeste', [celeste]),
|
|
row(3, 'Hade', [hades2, hades], {exact: false, playtimeMinutes: 90}),
|
|
],
|
|
};
|
|
|
|
// routes: 'METHOD path after /imports' -> response or (init) => response; Auth answers the favorite
|
|
const ShownUrl = () => {
|
|
const {pathname, search, hash} = useLocation();
|
|
return <output aria-label="url">{pathname + search + hash}</output>;
|
|
};
|
|
|
|
function renderPage(routes, entry = '/import') {
|
|
fetchRoutes({
|
|
'GET /api/users/me': json(200, {settings: {favoritePlatform: 'Switch'}}),
|
|
...Object.fromEntries(Object.entries(routes).map(([key, answer]) => {
|
|
const [method, path] = key.split(' ');
|
|
return [`${method} /api/library/imports${path}`,
|
|
typeof answer === 'function' ? (url, init) => answer(init) : answer];
|
|
})),
|
|
});
|
|
render(
|
|
<MemoryRouter initialEntries={[entry]}>
|
|
<Routes>
|
|
<Route path="/import" element={<PreferencesProvider><CurrentUserProvider><ImportPage/></CurrentUserProvider></PreferencesProvider>}/>
|
|
<Route path="/login" element={<p>login</p>}/>
|
|
</Routes>
|
|
<ShownUrl/>
|
|
</MemoryRouter>,
|
|
);
|
|
}
|
|
|
|
const storeData = (games) => btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(games))));
|
|
|
|
const posted = (method, path) => fetch.mock.calls.find(([url, init]) => init?.method === method && url.endsWith(path));
|
|
const polls = (path) => fetch.mock.calls.filter(([url, init]) => !init?.method && url.endsWith(path)).length;
|
|
const wait = (ms) => act(() => vi.advanceTimersByTimeAsync(ms));
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
localStorage.setItem('tbp_session', '1');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.useRealTimers();
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('initialChoice', () => {
|
|
it('checks only a sure match with a platform', () => {
|
|
expect(initialChoice(ready.rows[0], 'Switch')).toMatchObject({igdbId: 1, platform: 'Switch', checked: true});
|
|
expect(initialChoice(ready.rows[0], null)).toMatchObject({platform: '', checked: false});
|
|
expect(initialChoice(ready.rows[1], 'PC')).toMatchObject({platform: 'PC', checked: false});
|
|
expect(initialChoice(ready.rows[2], 'PC')).toMatchObject({platform: 'PC', checked: false, hours: '1.5'});
|
|
expect(initialChoice(row(4, 'x', []), 'PC')).toMatchObject({igdbId: null, checked: false});
|
|
});
|
|
});
|
|
|
|
describe('stepOf', () => {
|
|
it('maps the status to the step, and a failed import to the step it stopped in', () => {
|
|
expect(stepOf({status: 'QUEUED'})).toBe('QUEUED');
|
|
expect(stepOf({status: 'READY'})).toBe('REVIEW');
|
|
expect(stepOf({status: 'SAVING', step: null})).toBe('SAVING');
|
|
expect(stepOf({status: 'SAVING', step: 'DETAILS'})).toBe('DETAILS');
|
|
expect(stepOf({status: 'FAILED', chosen: 0})).toBe('MATCHING');
|
|
expect(stepOf({status: 'FAILED', chosen: 3})).toBe('SAVING');
|
|
expect(stepOf({status: 'UNDONE'})).toBe('DONE');
|
|
});
|
|
});
|
|
|
|
describe('ImportPage', () => {
|
|
it('shows a queued import with the imports before it', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'GET /8': json(200, {id: 8, source: 'CSV', status: 'QUEUED', total: 40, matched: 0, chosen: 0, saved: 0,
|
|
added: 0, ahead: 2, attempts: 0}),
|
|
}, '/import?id=8');
|
|
|
|
expect(await screen.findByText('IMPORT.WAITING:2')).toBeTruthy();
|
|
expect(screen.getByText('IMPORT.STEP.QUEUED').closest('li').getAttribute('aria-current')).toBe('step');
|
|
});
|
|
|
|
it('tries a failed import again and polls it', async () => {
|
|
let retried = false;
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'GET /6': () => json(200, {id: 6, source: 'TEXT', total: 10, matched: retried ? 4 : 2, chosen: 0, saved: 0,
|
|
added: 0, ahead: null, attempts: retried ? 0 : 3, status: retried ? 'MATCHING' : 'FAILED'}),
|
|
'POST /6/retry': () => {
|
|
retried = true;
|
|
return json(202, null);
|
|
},
|
|
}, '/import?id=6');
|
|
|
|
expect(await screen.findByText('IMPORT.FAILED:3')).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.RETRY'}));
|
|
|
|
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
|
expect(posted('POST', '/6/retry')).toBeTruthy();
|
|
});
|
|
|
|
it('starts an import from the list and shows the progress', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'POST ': json(202, {id: 9}),
|
|
'GET /9': json(200, {id: 9, source: 'TEXT', status: 'MATCHING', total: 2, matched: 1}),
|
|
});
|
|
fireEvent.change(await screen.findByLabelText('IMPORT.TEXT_LABEL'), {target: {value: 'Hades\n\n Celeste \n'}});
|
|
expect(screen.getByText('IMPORT.LINES:2')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.START'}));
|
|
|
|
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
|
expect(JSON.parse(posted('POST', '/imports')[1].body)).toEqual({
|
|
source: 'TEXT', rows: [{title: 'Hades'}, {title: 'Celeste'}],
|
|
});
|
|
});
|
|
|
|
it('reads an export file by its format and sends that as the source', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'POST ': json(202, {id: 9}),
|
|
'GET /9': json(200, {id: 9, source: 'GROUVEE', status: 'MATCHING', total: 1, matched: 0}),
|
|
});
|
|
fireEvent.click(await screen.findByRole('button', {name: 'IMPORT.SOURCE_CSV'}));
|
|
const file = new File(['id,name,shelves,platforms,dates\n1,Hades,"{""Playing"": {}}","{""PC"": {}}",None\n'],
|
|
'grouvee.csv', {type: 'text/csv'});
|
|
fireEvent.change(screen.getByLabelText('IMPORT.CSV_LABEL'), {target: {files: [file]}});
|
|
|
|
expect(await screen.findByText('IMPORT.FORMAT_READ')).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.START'}));
|
|
|
|
await screen.findByText('IMPORT.MATCHING');
|
|
expect(JSON.parse(posted('POST', '/imports')[1].body)).toEqual({
|
|
source: 'GROUVEE', rows: [{title: 'Hades', platform: 'PC', playtimeMinutes: null, status: 'PLAYING'}],
|
|
});
|
|
});
|
|
|
|
it('takes the store list from the URL, drops it there and sends it as BOOKMARKLET', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'POST ': json(202, {id: 9}),
|
|
'GET /9': json(200, {id: 9, source: 'BOOKMARKLET', status: 'MATCHING', total: 2, matched: 0}),
|
|
}, `/import#data=${storeData([['Hades', 'PC'], ['Pokémon Scarlet', 'Switch']])}`);
|
|
|
|
expect(await screen.findByText('IMPORT.STORE_READ:2')).toBeTruthy();
|
|
expect(screen.getByText('Pokémon Scarlet · Switch')).toBeTruthy();
|
|
// The router takes the new URL in a later render than the rows.
|
|
await waitFor(() => expect(screen.getByLabelText('url').textContent).toBe('/import'));
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.START'}));
|
|
|
|
await screen.findByText('IMPORT.MATCHING');
|
|
expect(JSON.parse(posted('POST', '/imports')[1].body)).toEqual({source: 'BOOKMARKLET', rows: [
|
|
{title: 'Hades', platform: 'PC', playtimeMinutes: null, status: null},
|
|
{title: 'Pokémon Scarlet', platform: 'Switch', playtimeMinutes: null, status: null},
|
|
]});
|
|
});
|
|
|
|
it('tells the user when the store list in the URL is broken', async () => {
|
|
renderPage({'GET ': json(200, {imports: []})}, '/import#data=broken');
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('IMPORT.STORE_LIST_BROKEN'));
|
|
await waitFor(() => expect(screen.getByLabelText('url').textContent).toBe('/import'));
|
|
expect(await screen.findByLabelText('IMPORT.TEXT_LABEL')).toBeTruthy();
|
|
});
|
|
|
|
it('says that the store list waits while another import runs', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: [{...ready, status: 'MATCHING', rows: null}]}),
|
|
'GET /5': json(200, {...ready, status: 'MATCHING', rows: null}),
|
|
}, `/import#data=${storeData([['Hades', 'PC']])}`);
|
|
|
|
await waitFor(() => expect(toast.info).toHaveBeenCalledWith('IMPORT.STORE_LIST_WAITS'));
|
|
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
|
});
|
|
|
|
it('treats a store list without a usable row as broken', async () => {
|
|
renderPage({'GET ': json(200, {imports: []})}, `/import#data=${btoa('[[1, "PC"]]')}`);
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('IMPORT.STORE_LIST_BROKEN'));
|
|
expect(await screen.findByLabelText('IMPORT.TEXT_LABEL')).toBeTruthy();
|
|
});
|
|
|
|
it('offers the bookmarklet as a link, not run on click', async () => {
|
|
renderPage({'GET ': json(200, {imports: []})});
|
|
fireEvent.click(await screen.findByRole('button', {name: 'IMPORT.SOURCE_BOOKMARKLET'}));
|
|
|
|
const link = screen.getByText('IMPORT.STORE_LINK');
|
|
expect(link.getAttribute('href')).toMatch(/^javascript:[^#]*$/);
|
|
expect(fireEvent.click(link)).toBe(false);
|
|
});
|
|
|
|
it('polls while matching and then shows the review', async () => {
|
|
let calls = 0;
|
|
renderPage({
|
|
'GET ': json(200, {imports: [{...ready, status: 'MATCHING', rows: null}]}),
|
|
'GET /5': () => (++calls === 1 ? json(200, {...ready, status: 'MATCHING', matched: 1, rows: null}) : json(200, ready)),
|
|
});
|
|
|
|
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
|
await wait(2000);
|
|
expect(await screen.findByText('IMPORT.LINE: Hades')).toBeTruthy();
|
|
});
|
|
|
|
it('preselects the favorite, checks only sure matches, and saves the checked lines', async () => {
|
|
let saved = false;
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'GET /5': () => json(200, saved ? {...ready, status: 'SAVED', chosen: 2, saved: 2, added: 1, rows: null} : ready),
|
|
'POST /5/save': () => {
|
|
saved = true;
|
|
return json(202, null);
|
|
},
|
|
}, '/import?id=5');
|
|
|
|
const first = await screen.findByLabelText('IMPORT.LINE: Hades');
|
|
await waitFor(() => expect(within(first.closest('li')).getByLabelText('IMPORT.PLATFORM').value).toBe('Switch'));
|
|
expect(first.checked).toBe(true);
|
|
// In the library already, and not the same name.
|
|
expect(screen.getByLabelText('IMPORT.LINE: Celeste').checked).toBe(false);
|
|
const third = screen.getByLabelText('IMPORT.LINE: Hade');
|
|
expect(third.checked).toBe(false);
|
|
|
|
const line3 = third.closest('li');
|
|
fireEvent.change(within(line3).getByLabelText('IMPORT.MATCH'), {target: {value: '1'}});
|
|
expect(within(line3).getByLabelText('IMPORT.PLATFORM').value).toBe('Switch');
|
|
fireEvent.change(within(line3).getByLabelText('IMPORT.STATUS_LABEL'), {target: {value: 'PLAYING'}});
|
|
fireEvent.click(third);
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.SAVE:2'}));
|
|
|
|
// The save runs in the background; the page polls until it is done.
|
|
expect(await screen.findByText('IMPORT.DONE')).toBeTruthy();
|
|
expect(JSON.parse(posted('POST', '/5/save')[1].body)).toEqual({rows: [
|
|
{lineNo: 1, igdbId: 1, platform: 'Switch', status: 'BACKLOG', playtimeMinutes: 0},
|
|
{lineNo: 3, igdbId: 1, platform: 'Switch', status: 'PLAYING', playtimeMinutes: 90},
|
|
]});
|
|
});
|
|
|
|
it('checks every line with a new match at once, and none', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /5': json(200, ready)}, '/import?id=5');
|
|
const checked = () => ['Hades', 'Celeste', 'Hade'].map((title) => screen.getByLabelText(`IMPORT.LINE: ${title}`).checked);
|
|
await screen.findByLabelText('IMPORT.LINE: Hades');
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.SELECT_ALL'}));
|
|
// Celeste is in the library already.
|
|
expect(checked()).toEqual([true, false, true]);
|
|
expect(screen.getByRole('button', {name: 'IMPORT.SAVE:2'})).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.SELECT_NONE'}));
|
|
expect(checked()).toEqual([false, false, false]);
|
|
expect(screen.getByRole('button', {name: 'IMPORT.SAVE:0'}).disabled).toBe(true);
|
|
});
|
|
|
|
it('keeps a line in the library that was checked by hand on select all', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /5': json(200, ready)}, '/import?id=5');
|
|
const celesteBox = await screen.findByLabelText('IMPORT.LINE: Celeste');
|
|
fireEvent.click(celesteBox);
|
|
expect(celesteBox.checked).toBe(true);
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.SELECT_ALL'}));
|
|
|
|
expect(screen.getByLabelText('IMPORT.LINE: Celeste').checked).toBe(true);
|
|
});
|
|
|
|
it('undoes a saved import after a confirm with the changed count', async () => {
|
|
renderPage({
|
|
'GET ': json(200, {imports: [{id: 4, source: 'CSV', status: 'SAVED', total: 3, matched: 3, createdAt: '2026-09-29T10:00:00Z'}]}),
|
|
'GET /4/undo-check': json(200, {games: 3, changed: 1}),
|
|
'DELETE /4/games': json(204, null),
|
|
});
|
|
|
|
fireEvent.click(await screen.findByRole('button', {name: 'IMPORT.UNDO'}));
|
|
expect(await screen.findByText('IMPORT.UNDO_CONFIRM:3')).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.UNDO_YES'}));
|
|
|
|
await waitFor(() => expect(toast.success).toHaveBeenCalledWith('IMPORT.UNDONE:3'));
|
|
expect(posted('DELETE', '/4/games')).toBeTruthy();
|
|
});
|
|
|
|
it('says so when another import is still matching', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'POST ': json(409, {code: 'IMPORT_RUNNING'})});
|
|
fireEvent.change(await screen.findByLabelText('IMPORT.TEXT_LABEL'), {target: {value: 'Hades'}});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'IMPORT.START'}));
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('IMPORT.RUNNING'));
|
|
});
|
|
|
|
it('stops polling on a 4xx and says so', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /abc': json(400, null)}, '/import?id=abc');
|
|
|
|
expect(await screen.findByText('IMPORT.LOAD_FAILED')).toBeTruthy();
|
|
});
|
|
|
|
it('keeps showing an import opened without an id when it leaves the worker', async () => {
|
|
let polls = 0;
|
|
const matching = {id: 5, source: 'TEXT', status: 'MATCHING', total: 3, matched: 1, chosen: 0, saved: 0, added: 0,
|
|
ahead: null, attempts: 0, rows: null};
|
|
renderPage({
|
|
'GET ': () => json(200, {imports: [polls > 1 ? {...ready, rows: null} : matching]}),
|
|
'GET /5': () => (++polls > 1 ? json(200, ready) : json(200, matching)),
|
|
});
|
|
|
|
await screen.findByText('IMPORT.MATCHING');
|
|
await wait(2000);
|
|
expect(await screen.findByText('IMPORT.LINE: Hades')).toBeTruthy();
|
|
});
|
|
|
|
it('keeps polling after a 502 while Core restarts', async () => {
|
|
let calls = 0;
|
|
renderPage({
|
|
'GET ': json(200, {imports: []}),
|
|
'GET /8': () => (++calls === 1 ? json(502, null) : json(200, {id: 8, source: 'TEXT', status: 'MATCHING',
|
|
total: 4, matched: 1, chosen: 0, saved: 0, added: 0, ahead: null, attempts: 0})),
|
|
}, '/import?id=8');
|
|
|
|
await waitFor(() => expect(polls('/8')).toBe(1));
|
|
await wait(4000);
|
|
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
|
});
|
|
|
|
it('stops polling after repeated 500', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /8': json(500, null)}, '/import?id=8');
|
|
|
|
await waitFor(() => expect(polls('/8')).toBe(1));
|
|
await wait(10 * 30_000);
|
|
expect(screen.getByText('IMPORT.LOAD_FAILED')).toBeTruthy();
|
|
expect(polls('/8')).toBe(10);
|
|
await wait(60_000);
|
|
expect(polls('/8')).toBe(10);
|
|
});
|
|
|
|
it('doubles the delay after each failure', async () => {
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /8': json(503, null)}, '/import?id=8');
|
|
|
|
await waitFor(() => expect(polls('/8')).toBe(1));
|
|
await wait(3500);
|
|
expect(polls('/8')).toBe(1);
|
|
await wait(1000);
|
|
expect(polls('/8')).toBe(2);
|
|
await wait(7000);
|
|
expect(polls('/8')).toBe(2);
|
|
await wait(1500);
|
|
expect(polls('/8')).toBe(3);
|
|
});
|
|
|
|
it('pauses polling in a hidden tab and goes on when it is visible again', async () => {
|
|
const hidden = vi.spyOn(document, 'hidden', 'get').mockReturnValue(true);
|
|
renderPage({'GET ': json(200, {imports: []}), 'GET /9': json(200, {id: 9, source: 'TEXT', status: 'MATCHING',
|
|
total: 2, matched: 1})}, '/import?id=9');
|
|
|
|
await screen.findByText('IMPORT.MATCHING');
|
|
await wait(20_000);
|
|
expect(polls('/9')).toBe(1);
|
|
|
|
hidden.mockReturnValue(false);
|
|
await act(async () => document.dispatchEvent(new Event('visibilitychange')));
|
|
expect(polls('/9')).toBe(2);
|
|
await wait(2000);
|
|
expect(polls('/9')).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('review findings', () => {
|
|
it('shows the review when Auth cannot be reached for the favorite', async () => {
|
|
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
fetchRoutes({
|
|
'GET /api/users/me': () => Promise.reject(new TypeError('Failed to fetch')),
|
|
'GET /api/library/imports': json(200, {imports: []}),
|
|
'GET /api/library/imports/5': json(200, ready),
|
|
});
|
|
render(
|
|
<MemoryRouter initialEntries={['/import?id=5']}>
|
|
<Routes>
|
|
<Route path="/import" element={<PreferencesProvider><CurrentUserProvider><ImportPage/></CurrentUserProvider></PreferencesProvider>}/>
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
|
|
expect(await screen.findByLabelText('IMPORT.LINE: Hades')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
|
|
describe('ImportPage 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/imports': json(401, {}),
|
|
'POST /api/auth/refresh': json(401, {}),
|
|
});
|
|
const {router} = renderWithProviders(<ImportPage/>, {path: '/import', entries: ['/import'], 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: '/import'});
|
|
expect(toast.error).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps the store list in the URL for the login when the session has expired', async () => {
|
|
fetchRoutes({
|
|
'GET /api/users/me': json(200, {settings: {}}),
|
|
'GET /api/library/imports': json(401, {}),
|
|
'POST /api/auth/refresh': json(401, {}),
|
|
});
|
|
const entry = `/import#data=${storeData([['Hades', 'PC']])}`;
|
|
const {router} = renderWithProviders(<ImportPage/>, {path: '/import', entries: [entry], 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: entry});
|
|
});
|
|
});
|