Files
ToBePlayed-Frontend/src/hooks/useSaveAppearance.test.jsx
T

228 lines
8.5 KiB
React

// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {act, cleanup, renderHook} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import {toast} from 'sonner';
import {PreferencesProvider, usePreferences} from '../context/PreferencesContext.jsx';
import useSaveAppearance from './useSaveAppearance.js';
import {DEFAULT_APPEARANCE} from '../utils/appearance.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
const wrapper = ({children}) => (
<MemoryRouter><PreferencesProvider>{children}</PreferencesProvider></MemoryRouter>
);
const renderSave = () => renderHook(() => ({save: useSaveAppearance(), preferences: usePreferences()}), {wrapper});
beforeEach(() => {
localStorage.setItem('tbp_jwt', 'token');
});
afterEach(() => {
cleanup();
localStorage.clear();
vi.clearAllMocks();
});
describe('useSaveAppearance', () => {
it('applies the change at once and sends the whole appearance', async () => {
globalThis.fetch = vi.fn(async () => ({ok: true, status: 204}));
const {result} = renderSave();
let saved;
await act(async () => {
saved = await result.current.save({accent: 'blue'});
});
expect(saved).toBe(true);
expect(result.current.preferences.appearance.accent).toBe('blue');
expect(document.documentElement.dataset.accent).toBe('blue');
const [url, request] = fetch.mock.calls[0];
expect(url).toContain('/api/users/settings/appearance');
expect(request.method).toBe('PUT');
expect(JSON.parse(request.body)).toEqual({accent: 'blue', corners: 'default', wideLayout: false, backgroundAppid: null});
});
it('puts the old value back when Auth refuses', async () => {
globalThis.fetch = vi.fn(async () => ({ok: false, status: 400}));
const {result} = renderSave();
await act(async () => {
await result.current.save({corners: 'round'});
});
expect(result.current.preferences.appearance.corners).toBe('default');
expect(toast.error).toHaveBeenCalledWith('SETTINGS.ERROR_SAVE');
});
it('keeps a later change when an earlier save fails', async () => {
let failFirst;
globalThis.fetch = vi.fn()
.mockImplementationOnce(() => new Promise((resolve) => {
failFirst = () => resolve({ok: false, status: 500});
}))
.mockImplementation(async () => ({ok: true, status: 204}));
const {result} = renderSave();
let first;
let second;
await act(async () => {
first = result.current.save({accent: 'green'});
});
await act(async () => {
second = result.current.save({accent: 'pink'});
});
await act(async () => {
failFirst();
await first;
await second;
});
expect(result.current.preferences.appearance.accent).toBe('pink');
expect(JSON.parse(fetch.mock.calls[1][1].body).accent).toBe('pink');
});
it('ends the session on a 401', async () => {
globalThis.fetch = vi.fn(async () => ({ok: false, status: 401}));
const {result} = renderSave();
await act(async () => {
await result.current.save({wideLayout: true});
});
expect(localStorage.getItem('tbp_jwt')).toBeNull();
});
it('ends the session with the default look on a 401, so the next user does not get this one', async () => {
localStorage.setItem('tbp_appearance', JSON.stringify({accent: 'green', corners: 'round', wideLayout: true, backgroundAppid: 620}));
globalThis.fetch = vi.fn(async () => ({ok: false, status: 401}));
const {result} = renderSave();
await act(async () => {
await result.current.save({corners: 'sharp'});
});
expect(result.current.preferences.appearance).toEqual(DEFAULT_APPEARANCE);
expect(JSON.parse(localStorage.getItem('tbp_appearance'))).toEqual(DEFAULT_APPEARANCE);
});
it('waits for a running load from Auth, then keeps its values for the fields not changed here', async () => {
const fromServer = {accent: 'green', corners: 'default', wideLayout: false, backgroundAppid: null};
let answerMe;
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
? Promise.resolve({ok: true, status: 204})
: new Promise((resolve) => {
answerMe = () => resolve({ok: true, status: 200, json: async () => ({settings: {appearance: fromServer}})});
})));
const {result} = renderSave();
let sync;
let save;
act(() => {
sync = result.current.preferences.syncAppearance();
});
act(() => {
save = result.current.save({corners: 'round'});
});
expect(result.current.preferences.appearance.corners).toBe('round');
expect(fetch.mock.calls.some(([, init]) => init?.method === 'PUT')).toBe(false);
await act(async () => {
answerMe();
await sync;
await save;
});
expect(result.current.preferences.appearance).toEqual({...fromServer, corners: 'round'});
const [, put] = fetch.mock.calls.find(([, init]) => init?.method === 'PUT');
expect(JSON.parse(put.body)).toEqual({...fromServer, corners: 'round'});
});
it('does not bring back the look of the user before after a reset', async () => {
let answerMe;
globalThis.fetch = vi.fn(() => new Promise((resolve) => {
answerMe = () => resolve({ok: true, status: 200, json: async () => ({settings: {appearance: {accent: 'red', corners: 'sharp'}}})});
}));
const {result} = renderSave();
let sync;
act(() => {
sync = result.current.preferences.syncAppearance();
});
act(() => result.current.preferences.resetAppearance());
await act(async () => {
answerMe();
await sync;
});
expect(result.current.preferences.appearance).toEqual(DEFAULT_APPEARANCE);
});
it('sends saves one at a time, so a later save never carries an older value', async () => {
const answers = [];
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
? new Promise((resolve) => answers.push(() => resolve({ok: true, status: 204})))
: Promise.resolve({ok: true, status: 200, json: async () => ({})})));
const puts = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT').map(([, init]) => JSON.parse(init.body));
const {result} = renderSave();
let first;
let second;
await act(async () => {
first = result.current.save({accent: 'green'});
});
await act(async () => {
second = result.current.save({corners: 'round'});
});
expect(puts()).toHaveLength(1);
await act(async () => {
answers[0]();
await first;
});
await act(async () => {
answers[1]();
await second;
});
let third;
await act(async () => {
third = result.current.save({wideLayout: true});
});
await act(async () => {
answers[2]();
await third;
});
expect(result.current.preferences.appearance.corners).toBe('round');
expect(puts()[2]).toEqual({accent: 'green', corners: 'round', wideLayout: true, backgroundAppid: null});
});
it('sends nothing for a change that waited for the load when a reset came first', async () => {
let answerMe;
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
? Promise.resolve({ok: true, status: 204})
: new Promise((resolve) => {
answerMe = () => resolve({ok: true, status: 200, json: async () => ({})});
})));
const {result} = renderSave();
act(() => {
result.current.preferences.syncAppearance();
});
let save;
act(() => {
save = result.current.save({accent: 'green'});
});
act(() => result.current.preferences.resetAppearance());
await act(async () => {
answerMe();
await save;
});
expect(fetch.mock.calls.some(([, init]) => init?.method === 'PUT')).toBe(false);
expect(result.current.preferences.appearance).toEqual(DEFAULT_APPEARANCE);
});
});