Public Access
196 lines
8.1 KiB
React
196 lines
8.1 KiB
React
// @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 BacklogPage from './BacklogPage.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, extra = {}) => ({appid, name: `Game ${appid}`, priority: null, timeToBeatMinutes: null, ...extra});
|
|
const backlog = {games: [game(1), game(2), game(3), game(4, {coming: true, comingOn: '2099-01-01'})]};
|
|
|
|
// The providers load the user and the preferences; this page does not use them.
|
|
const withAuth = (routes) => fetchRoutes({'GET /api/users/me': json(200, {settings: {}}), ...routes});
|
|
|
|
const renderPage = () => renderWithProviders(<BacklogPage/>,
|
|
{path: '/backlog', entries: ['/backlog'], guarded: true, routes: {'/library': <p>library</p>}});
|
|
|
|
const names = () => [...document.querySelectorAll('ol a[href^="/library/"]')].map((a) => a.textContent);
|
|
const puts = () => globalThis.fetch.mock.calls.filter(([, init]) => init?.method === 'PUT')
|
|
.map(([, init]) => JSON.parse(init.body).appids);
|
|
const rowButton = (rowName, label) => {
|
|
const row = screen.getByText(rowName).closest('li');
|
|
return [...row.querySelectorAll('button')].find((b) => b.getAttribute('aria-label') === label);
|
|
};
|
|
|
|
describe('BacklogPage', () => {
|
|
beforeEach(() => {
|
|
localStorage.setItem('tbp_session', '1');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('shows the games in the order of the server, a coming game too', async () => {
|
|
withAuth({'GET /api/library/backlog': json(200, backlog)});
|
|
renderPage();
|
|
|
|
await screen.findByText('Game 1');
|
|
expect(names()).toEqual(['Game 1', 'Game 2', 'Game 3', 'Game 4']);
|
|
expect(screen.getByText('LIBRARY.COMING', {exact: false})).toBeTruthy();
|
|
});
|
|
|
|
it('moves a game up and down with the arrows and sends the whole list', async () => {
|
|
withAuth({
|
|
'GET /api/library/backlog': json(200, backlog),
|
|
'PUT /api/library/backlog': json(204, {}),
|
|
});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
fireEvent.click(rowButton('Game 3', 'BACKLOG_ORDER.MOVE_UP'));
|
|
await waitFor(() => expect(puts()).toEqual([[1, 3, 2, 4]]));
|
|
expect(names()).toEqual(['Game 1', 'Game 3', 'Game 2', 'Game 4']);
|
|
|
|
fireEvent.click(rowButton('Game 1', 'BACKLOG_ORDER.MOVE_DOWN'));
|
|
await waitFor(() => expect(puts()).toEqual([[1, 3, 2, 4], [3, 1, 2, 4]]));
|
|
});
|
|
|
|
it('does not offer to move the first game up or the last game down', async () => {
|
|
withAuth({'GET /api/library/backlog': json(200, backlog)});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
expect(rowButton('Game 1', 'BACKLOG_ORDER.MOVE_UP').disabled).toBe(true);
|
|
expect(rowButton('Game 4', 'BACKLOG_ORDER.MOVE_DOWN').disabled).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
['BACKLOG_ORDER.MOVE_TOP', 'Game 3', [3, 1, 2, 4]],
|
|
['BACKLOG_ORDER.MOVE_BOTTOM', 'Game 2', [1, 3, 4, 2]],
|
|
])('%s from the menu sends the new order', async (item, row, expected) => {
|
|
withAuth({
|
|
'GET /api/library/backlog': json(200, backlog),
|
|
'PUT /api/library/backlog': json(204, {}),
|
|
});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
fireEvent.keyDown(rowButton(row, 'BACKLOG_ORDER.MORE'), {key: 'Enter'});
|
|
fireEvent.click(await screen.findByRole('menuitem', {name: item}));
|
|
|
|
await waitFor(() => expect(puts()).toEqual([expected]));
|
|
});
|
|
|
|
it('moves a game with the drag handle to the row under the pointer', async () => {
|
|
withAuth({
|
|
'GET /api/library/backlog': json(200, backlog),
|
|
'PUT /api/library/backlog': json(204, {}),
|
|
});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
document.querySelectorAll('ol > li').forEach((row, i) => {
|
|
row.getBoundingClientRect = () => ({top: i * 100, height: 100});
|
|
});
|
|
const grip = rowButton('Game 1', 'BACKLOG_ORDER.MOVE_DRAG');
|
|
|
|
fireEvent.pointerDown(grip, {button: 0, pointerId: 1});
|
|
fireEvent.pointerMove(grip, {clientY: 240, pointerId: 1});
|
|
fireEvent.pointerUp(grip, {pointerId: 1});
|
|
|
|
await waitFor(() => expect(puts()).toEqual([[2, 3, 1, 4]]));
|
|
expect(names()).toEqual(['Game 2', 'Game 3', 'Game 1', 'Game 4']);
|
|
});
|
|
|
|
it('sends nothing when a drag ends on the row it started on, or is cancelled', async () => {
|
|
withAuth({'GET /api/library/backlog': json(200, backlog)});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
document.querySelectorAll('ol > li').forEach((row, i) => {
|
|
row.getBoundingClientRect = () => ({top: i * 100, height: 100});
|
|
});
|
|
const grip = rowButton('Game 1', 'BACKLOG_ORDER.MOVE_DRAG');
|
|
|
|
fireEvent.pointerDown(grip, {button: 0, pointerId: 1});
|
|
fireEvent.pointerUp(grip, {pointerId: 1});
|
|
fireEvent.pointerDown(grip, {button: 0, pointerId: 1});
|
|
fireEvent.pointerMove(grip, {clientY: 340, pointerId: 1});
|
|
fireEvent.pointerCancel(grip, {pointerId: 1});
|
|
|
|
expect(puts()).toEqual([]);
|
|
expect(names()).toEqual(['Game 1', 'Game 2', 'Game 3', 'Game 4']);
|
|
});
|
|
|
|
it('shows a toast and loads the list again when saving fails', async () => {
|
|
let loads = 0;
|
|
withAuth({
|
|
'GET /api/library/backlog': () => json(200, ++loads === 1 ? backlog : {games: [game(2), game(1)]}),
|
|
'PUT /api/library/backlog': json(500, {}),
|
|
});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
fireEvent.click(rowButton('Game 2', 'BACKLOG_ORDER.MOVE_UP'));
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('BACKLOG_ORDER.SAVE_FAILED'));
|
|
await waitFor(() => expect(names()).toEqual(['Game 2', 'Game 1']));
|
|
});
|
|
|
|
it('sends only the newest list after a running save, so the server gets them in order', async () => {
|
|
let release;
|
|
const first = new Promise((resolve) => { release = resolve; });
|
|
let puts_ = 0;
|
|
withAuth({
|
|
'GET /api/library/backlog': json(200, backlog),
|
|
'PUT /api/library/backlog': async () => {
|
|
if (++puts_ === 1) await first;
|
|
return json(204, {});
|
|
},
|
|
});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
fireEvent.click(rowButton('Game 2', 'BACKLOG_ORDER.MOVE_UP'));
|
|
fireEvent.click(rowButton('Game 3', 'BACKLOG_ORDER.MOVE_UP'));
|
|
fireEvent.click(rowButton('Game 4', 'BACKLOG_ORDER.MOVE_UP'));
|
|
expect(puts()).toEqual([[2, 1, 3, 4]]);
|
|
|
|
release();
|
|
|
|
await waitFor(() => expect(puts()).toEqual([[2, 1, 3, 4], [2, 3, 4, 1]]));
|
|
});
|
|
|
|
it('explains an empty backlog and links to the library', async () => {
|
|
withAuth({'GET /api/library/backlog': json(200, {games: []})});
|
|
renderPage();
|
|
|
|
expect(await screen.findByText('BACKLOG_ORDER.EMPTY')).toBeTruthy();
|
|
// The tab and the link of the empty card.
|
|
expect(screen.getAllByRole('link', {name: 'LIBRARY.TITLE'})).toHaveLength(2);
|
|
});
|
|
|
|
it('shows an error when the list cannot be loaded', async () => {
|
|
withAuth({'GET /api/library/backlog': json(500, {})});
|
|
renderPage();
|
|
|
|
expect(await screen.findByText('BACKLOG_ORDER.LOAD_FAILED')).toBeTruthy();
|
|
});
|
|
|
|
it('lets only the drag handle stop the page scroll', async () => {
|
|
withAuth({'GET /api/library/backlog': json(200, backlog)});
|
|
renderPage();
|
|
await screen.findByText('Game 1');
|
|
|
|
expect(rowButton('Game 1', 'BACKLOG_ORDER.MOVE_DRAG').className).toContain('touch-none');
|
|
const row = screen.getByText('Game 1').closest('li');
|
|
expect(row.className).not.toContain('touch-none');
|
|
expect(screen.getByText('Game 1').closest('ol').className).not.toContain('touch-none');
|
|
});
|
|
});
|