Public Access
feat(import): bookmarklet for store pages
This commit is contained in:
1 parent
c6d76b4cb5
commit
f01ad03f6e
8 files changed
+411
-22
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// @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} from 'react-router-dom';
|
||||
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';
|
||||
@@ -11,7 +11,7 @@ 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()}}));
|
||||
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};
|
||||
@@ -29,6 +29,11 @@ const ready = {
|
||||
};
|
||||
|
||||
// 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'}}),
|
||||
@@ -44,10 +49,13 @@ function renderPage(routes, entry = '/import') {
|
||||
<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));
|
||||
@@ -154,6 +162,59 @@ describe('ImportPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
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'));
|
||||
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({
|
||||
@@ -360,4 +421,18 @@ describe('ImportPage session', () => {
|
||||
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});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user