Public Access
232 lines
8.9 KiB
React
232 lines
8.9 KiB
React
// @vitest-environment jsdom
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {act, cleanup, renderHook} from '@testing-library/react';
|
|
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
|
import {toast} from 'sonner';
|
|
import {PreferencesProvider, usePreferences} from '../context/PreferencesContext.jsx';
|
|
import useSaveAppearance from './useSaveAppearance.js';
|
|
import RequireSession from '../components/RequireSession.jsx';
|
|
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()}}));
|
|
|
|
// RequireSession ends the session on a 401, as on the pages that use the hook.
|
|
const wrapper = ({children}) => (
|
|
<MemoryRouter><PreferencesProvider>
|
|
<Routes><Route element={<RequireSession/>}><Route path="*" element={children}/></Route></Routes>
|
|
</PreferencesProvider></MemoryRouter>
|
|
);
|
|
|
|
const renderSave = () => renderHook(() => ({save: useSaveAppearance(), preferences: usePreferences()}), {wrapper});
|
|
|
|
beforeEach(() => {
|
|
localStorage.setItem('tbp_session', '1');
|
|
});
|
|
|
|
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, bannerAppid: null, showBanner: true});
|
|
});
|
|
|
|
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, bannerAppid: 620, showBanner: false};
|
|
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.syncPreferences();
|
|
});
|
|
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.syncPreferences();
|
|
});
|
|
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, bannerAppid: null, showBanner: true});
|
|
});
|
|
|
|
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.syncPreferences();
|
|
});
|
|
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);
|
|
});
|
|
});
|