Public Access
Library rows: the status badge is a menu that saves at once. Game page: status and rating in the top card, saved on click. Status changes and hiding a game show an Undo toast. Other game actions moved into a "..." menu; split and unlink ask first. New sorts: name, last played, time to beat; "last synced" is gone. Page numbers and 44 px touch targets on phones.
110 lines
4.7 KiB
React
110 lines
4.7 KiB
React
// @vitest-environment jsdom
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {act, cleanup, fireEvent, screen, waitFor} from '@testing-library/react';
|
|
import {toast} from 'sonner';
|
|
import GameHeroCard from './GameHeroCard.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: 220, name: 'Half-Life 2', status: 'BACKLOG', rating: null, hidden: false, playtimeMinutes: 10};
|
|
|
|
const routes = (extra = {}) => fetchRoutes({
|
|
'GET /api/users/me': json(200, {settings: {}}),
|
|
'PUT /api/library/games/220/status': (url, init) => json(200, {...game, ...JSON.parse(init.body)}),
|
|
'PATCH /api/library/games/220/backlog': (url, init) => json(200, {...game, ...JSON.parse(init.body)}),
|
|
'PUT /api/library/games/220/hidden': (url, init) => json(200, JSON.parse(init.body)),
|
|
...extra,
|
|
});
|
|
|
|
const renderCard = (props = {}) => renderWithProviders(
|
|
<GameHeroCard game={game} auditor={false} onCorrect={vi.fn()} onMerge={vi.fn()} onHiddenChanged={vi.fn()}
|
|
onProfileStyleSaved={vi.fn()} onSaved={vi.fn()} {...props}/>);
|
|
|
|
const openMore = async () => fireEvent.keyDown(await screen.findByRole('button', {name: 'GAME_DETAIL.MORE'}), {key: 'Enter'});
|
|
const calls = (method) => fetch.mock.calls.filter(([, init]) => init?.method === method);
|
|
|
|
beforeEach(() => {
|
|
localStorage.setItem('tbp_session', '1');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
localStorage.clear();
|
|
});
|
|
|
|
describe('GameHeroCard', () => {
|
|
it('keepsTheMoreMenuForArtHideAndAuditorTools', async () => {
|
|
routes();
|
|
renderCard({auditor: true, profileStyle: {}});
|
|
expect(screen.getByRole('link', {name: /GAME_DETAIL.OPEN_IN_STEAM/})).toBeTruthy();
|
|
|
|
await openMore();
|
|
|
|
const names = (await screen.findAllByRole('menuitem')).map((item) => item.textContent);
|
|
expect(names).toEqual(['GAME_DETAIL.SET_BACKGROUND', 'GAME_DETAIL.SET_BANNER', 'GAME_DETAIL.HIDE', 'CORRECTION.OPEN', 'MERGE.OPEN']);
|
|
expect(screen.getAllByRole('menuitemcheckbox')).toHaveLength(2);
|
|
expect(screen.queryByRole('button', {name: 'CORRECTION.OPEN'})).toBeNull();
|
|
});
|
|
|
|
it('offersNoAuditorToolsToAUser', async () => {
|
|
routes();
|
|
renderCard();
|
|
|
|
await openMore();
|
|
|
|
const names = (await screen.findAllByRole('menuitem')).map((item) => item.textContent);
|
|
expect(names).not.toContain('CORRECTION.OPEN');
|
|
expect(names).not.toContain('MERGE.OPEN');
|
|
});
|
|
|
|
it('hidesWithAnUndoThatShowsTheGameAgain', async () => {
|
|
routes();
|
|
const onHiddenChanged = vi.fn();
|
|
renderCard({onHiddenChanged});
|
|
await openMore();
|
|
|
|
fireEvent.click(await screen.findByRole('menuitem', {name: 'GAME_DETAIL.HIDE'}));
|
|
|
|
await waitFor(() => expect(onHiddenChanged).toHaveBeenCalledWith(true));
|
|
const [message, options] = toast.success.mock.calls.at(-1);
|
|
expect(message).toBe('GAME_DETAIL.HIDDEN_TOAST');
|
|
expect(options.action.label).toBe('MANUAL_GAME.UNDO');
|
|
|
|
await act(async () => options.action.onClick());
|
|
|
|
expect(onHiddenChanged).toHaveBeenLastCalledWith(false);
|
|
expect(calls('PUT').map(([, init]) => JSON.parse(init.body))).toEqual([{hidden: true}, {hidden: false}]);
|
|
// The undo is a plain toast.
|
|
expect(toast.success.mock.calls.at(-1)).toEqual(['GAME_DETAIL.SHOWN_TOAST']);
|
|
});
|
|
|
|
it('savesTheStatusFromTheMenuWithoutAskingThePointsItself', async () => {
|
|
routes();
|
|
const onSaved = vi.fn();
|
|
renderCard({onSaved});
|
|
|
|
fireEvent.keyDown(screen.getByRole('button', {name: /BACKLOG.STATUS_LABEL/}), {key: 'Enter'});
|
|
fireEvent.click(await screen.findByRole('menuitemradio', {name: 'BACKLOG.STATUS.PLAYING'}));
|
|
|
|
await waitFor(() => expect(onSaved).toHaveBeenCalledWith(expect.objectContaining({status: 'PLAYING'})));
|
|
expect(JSON.parse(calls('PUT')[0][1].body)).toEqual({status: 'PLAYING'});
|
|
// GameDetail's GamePointsLine does the points check when the status changes; none here (no route would fail).
|
|
expect(toast.success.mock.calls.at(-1)[1].action.label).toBe('MANUAL_GAME.UNDO');
|
|
});
|
|
|
|
it('savesTheRatingAtOnce', async () => {
|
|
routes();
|
|
const onSaved = vi.fn();
|
|
renderCard({onSaved});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: 'BACKLOG.RATING_STARS:4'}));
|
|
|
|
await waitFor(() => expect(onSaved).toHaveBeenCalled());
|
|
expect(calls('PATCH').map(([, init]) => JSON.parse(init.body))).toEqual([{rating: 4}]);
|
|
});
|
|
});
|