Public Access
refactor(hooks): add useSetStatus for game status saves
Replaces 5 copies of the save code in hints, PlayNextPage and sync. An empty 2xx body now counts as saved, not a network error. Refs #46
This commit is contained in:
1 parent
bbc707de87
commit
73ad4fd3b8
10 files changed
+239
-114
No files matched your search
@@ -0,0 +1,160 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {act, cleanup, renderHook} from '@testing-library/react';
|
||||
import {toast} from 'sonner';
|
||||
import useSetStatus from './useSetStatus.js';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
const pointsCheck = vi.fn(() => Promise.resolve(null));
|
||||
vi.mock('./usePointsCheck.js', () => ({default: () => pointsCheck}));
|
||||
|
||||
const PUT = 'PUT /api/library/games/220/status';
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const render = (options) => renderHook(() => useSetStatus(220, options));
|
||||
|
||||
describe('useSetStatus', () => {
|
||||
it('savesTheStatusAndShowsTheSavedToast', async () => {
|
||||
const fetchMock = fetchRoutes({[PUT]: () => json(200, {status: 'PLAYING'})});
|
||||
const {result} = render();
|
||||
|
||||
let saved;
|
||||
await act(async () => {
|
||||
saved = await result.current.setStatus('PLAYING');
|
||||
});
|
||||
|
||||
expect(saved).toBe(true);
|
||||
expect(toast.success).toHaveBeenCalledWith('BACKLOG.SAVED');
|
||||
const [, init] = fetchMock.mock.calls[0];
|
||||
expect(JSON.parse(init.body)).toEqual({status: 'PLAYING'});
|
||||
});
|
||||
|
||||
it('passesTheSavedEntryToOnChanged', async () => {
|
||||
fetchRoutes({[PUT]: () => json(200, {status: 'PLAYING', appid: 220})});
|
||||
const onChanged = vi.fn();
|
||||
const {result} = render({onChanged});
|
||||
|
||||
await act(() => result.current.setStatus('PLAYING'));
|
||||
|
||||
expect(onChanged).toHaveBeenCalledTimes(1);
|
||||
expect(onChanged).toHaveBeenCalledWith({status: 'PLAYING', appid: 220});
|
||||
});
|
||||
|
||||
it('countsAnEmptyBodyAsSavedAndPassesNull', async () => {
|
||||
fetchRoutes({[PUT]: () => new Response(null, {status: 200})});
|
||||
const onChanged = vi.fn();
|
||||
const {result} = render({onChanged});
|
||||
|
||||
let saved;
|
||||
await act(async () => {
|
||||
saved = await result.current.setStatus('PLAYING');
|
||||
});
|
||||
|
||||
expect(saved).toBe(true);
|
||||
expect(toast.success).toHaveBeenCalledWith('BACKLOG.SAVED');
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
expect(onChanged).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('showsSaveFailedOnAServerError', async () => {
|
||||
fetchRoutes({[PUT]: () => json(500, {})});
|
||||
const onChanged = vi.fn();
|
||||
const {result} = render({onChanged});
|
||||
|
||||
await act(() => result.current.setStatus('PLAYING'));
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('BACKLOG.SAVE_FAILED');
|
||||
expect(onChanged).not.toHaveBeenCalled();
|
||||
expect(result.current.busy).toBe(false);
|
||||
});
|
||||
|
||||
it('showsTheNetworkErrorWhenFetchRejects', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
fetchRoutes({[PUT]: () => {
|
||||
throw new Error('offline');
|
||||
}});
|
||||
const {result} = render();
|
||||
|
||||
await act(() => result.current.setStatus('PLAYING'));
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('INFO.ERROR.NETWORK_FAILURE');
|
||||
});
|
||||
|
||||
it('staysSilentOn401', async () => {
|
||||
fetchRoutes({[PUT]: () => json(401, {}), 'POST /api/auth/refresh': () => json(401, {})});
|
||||
const onChanged = vi.fn();
|
||||
const {result} = render({onChanged});
|
||||
|
||||
let saved;
|
||||
await act(async () => {
|
||||
saved = await result.current.setStatus('PLAYING');
|
||||
});
|
||||
|
||||
expect(saved).toBe(false);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
expect(toast.success).not.toHaveBeenCalled();
|
||||
expect(onChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sendsOneRequestWhenCalledTwice', async () => {
|
||||
let release;
|
||||
const held = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const fetchMock = fetchRoutes({[PUT]: () => held});
|
||||
const {result} = render();
|
||||
|
||||
await act(async () => {
|
||||
const first = result.current.setStatus('PLAYING');
|
||||
const second = await result.current.setStatus('PLAYING');
|
||||
expect(second).toBe(false);
|
||||
release(json(200, {}));
|
||||
await first;
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('checksPointsOnlyWhenAsked', async () => {
|
||||
fetchRoutes({[PUT]: () => json(200, {})});
|
||||
const plain = render();
|
||||
await act(() => plain.result.current.setStatus('PLAYING'));
|
||||
expect(pointsCheck).not.toHaveBeenCalled();
|
||||
|
||||
const asking = render({checkPoints: true});
|
||||
await act(() => asking.result.current.setStatus('PLAYING'));
|
||||
expect(pointsCheck).toHaveBeenCalledWith(220, {status: 'PLAYING', tell: true});
|
||||
});
|
||||
|
||||
it('isBusyWhileSaving', async () => {
|
||||
let release;
|
||||
const held = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
fetchRoutes({[PUT]: () => held});
|
||||
const {result} = render();
|
||||
|
||||
let pending;
|
||||
await act(async () => {
|
||||
pending = result.current.setStatus('PLAYING');
|
||||
});
|
||||
expect(result.current.busy).toBe(true);
|
||||
await act(async () => {
|
||||
release(json(200, {}));
|
||||
await pending;
|
||||
});
|
||||
|
||||
expect(result.current.busy).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user