160 lines
6.0 KiB
JavaScript
160 lines
6.0 KiB
JavaScript
import { renderHook, act } from '@testing-library/react';
|
|
import { DEFAULT_PREFERENCES, writePreferences, usePreferences, useUpdatePreferences } from '@/lib/preferences';
|
|
|
|
const STORAGE_KEY = 'doormilePreferences';
|
|
|
|
/**
|
|
* Operator display preferences.
|
|
*
|
|
* Two properties matter: unknown/corrupt storage must never take a page down,
|
|
* and a change must reach EVERY mounted consumer including the one that made it
|
|
* — `storage` alone fires only in other tabs, which left the table that
|
|
* triggered the change unchanged.
|
|
*/
|
|
describe('preferences', () => {
|
|
describe('defaults', () => {
|
|
it('should default to comfortable density and a 25-row page', () => {
|
|
expect(DEFAULT_PREFERENCES).toEqual({ compactDensity: false, defaultPageSize: 25 });
|
|
});
|
|
|
|
it('should read the defaults when nothing is stored', () => {
|
|
const { result } = renderHook(() => usePreferences());
|
|
expect(result.current).toEqual(DEFAULT_PREFERENCES);
|
|
});
|
|
|
|
it('should merge a partial stored record over the defaults', () => {
|
|
// A record written by an older build must not leave newer keys undefined.
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ compactDensity: true }));
|
|
const { result } = renderHook(() => usePreferences());
|
|
expect(result.current).toEqual({ compactDensity: true, defaultPageSize: 25 });
|
|
});
|
|
|
|
it('should fall back to the defaults for a corrupt stored record', () => {
|
|
localStorage.setItem(STORAGE_KEY, '{not json');
|
|
const { result } = renderHook(() => usePreferences());
|
|
expect(result.current).toEqual(DEFAULT_PREFERENCES);
|
|
});
|
|
|
|
it('should not hand out the shared defaults object itself', () => {
|
|
// A consumer mutating what it reads would silently rewrite the defaults
|
|
// for every other consumer.
|
|
const { result } = renderHook(() => usePreferences());
|
|
expect(result.current).not.toBe(DEFAULT_PREFERENCES);
|
|
});
|
|
});
|
|
|
|
describe('writePreferences', () => {
|
|
it('should persist a patch merged over what is already stored', () => {
|
|
writePreferences({ compactDensity: true });
|
|
writePreferences({ defaultPageSize: 100 });
|
|
expect(JSON.parse(localStorage.getItem(STORAGE_KEY))).toEqual({
|
|
compactDensity: true,
|
|
defaultPageSize: 100
|
|
});
|
|
});
|
|
|
|
it('should return the merged result to the caller', () => {
|
|
expect(writePreferences({ defaultPageSize: 50 })).toEqual({
|
|
compactDensity: false,
|
|
defaultPageSize: 50
|
|
});
|
|
});
|
|
|
|
it('should broadcast the change so the triggering component re-renders too', () => {
|
|
const listener = jest.fn();
|
|
window.addEventListener('doormile:preferences', listener);
|
|
writePreferences({ compactDensity: true });
|
|
window.removeEventListener('doormile:preferences', listener);
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener.mock.calls[0][0].detail).toEqual({ compactDensity: true, defaultPageSize: 25 });
|
|
});
|
|
|
|
it('should still broadcast when the write itself is refused', () => {
|
|
// Private mode / quota: the change applies for this session either way,
|
|
// so the UI must not be left showing the old value.
|
|
const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('QuotaExceededError');
|
|
});
|
|
const listener = jest.fn();
|
|
window.addEventListener('doormile:preferences', listener);
|
|
|
|
expect(() => writePreferences({ compactDensity: true })).not.toThrow();
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
|
|
window.removeEventListener('doormile:preferences', listener);
|
|
setItem.mockRestore();
|
|
});
|
|
|
|
it('should accept an empty patch as a no-op that still returns the current values', () => {
|
|
expect(writePreferences({})).toEqual(DEFAULT_PREFERENCES);
|
|
});
|
|
});
|
|
|
|
describe('usePreferences', () => {
|
|
it('should re-render with the new value when a preference changes', () => {
|
|
const { result } = renderHook(() => usePreferences());
|
|
expect(result.current.compactDensity).toBe(false);
|
|
|
|
act(() => {
|
|
writePreferences({ compactDensity: true });
|
|
});
|
|
|
|
expect(result.current.compactDensity).toBe(true);
|
|
});
|
|
|
|
it('should pick up a change made in another tab', () => {
|
|
const { result } = renderHook(() => usePreferences());
|
|
|
|
act(() => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ defaultPageSize: 100 }));
|
|
window.dispatchEvent(new Event('storage'));
|
|
});
|
|
|
|
expect(result.current.defaultPageSize).toBe(100);
|
|
});
|
|
|
|
it('should keep every mounted consumer in agreement', () => {
|
|
const a = renderHook(() => usePreferences());
|
|
const b = renderHook(() => usePreferences());
|
|
|
|
act(() => {
|
|
writePreferences({ defaultPageSize: 50 });
|
|
});
|
|
|
|
expect(a.result.current).toEqual(b.result.current);
|
|
expect(a.result.current.defaultPageSize).toBe(50);
|
|
});
|
|
|
|
it('should stop listening once unmounted', () => {
|
|
const removeSpy = jest.spyOn(window, 'removeEventListener');
|
|
const { unmount } = renderHook(() => usePreferences());
|
|
unmount();
|
|
|
|
expect(removeSpy).toHaveBeenCalledWith('doormile:preferences', expect.any(Function));
|
|
expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe('useUpdatePreferences', () => {
|
|
it('should write and broadcast through the same path as writePreferences', () => {
|
|
const update = renderHook(() => useUpdatePreferences());
|
|
const read = renderHook(() => usePreferences());
|
|
|
|
act(() => {
|
|
update.result.current({ compactDensity: true });
|
|
});
|
|
|
|
expect(read.result.current.compactDensity).toBe(true);
|
|
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)).compactDensity).toBe(true);
|
|
});
|
|
|
|
it('should keep a stable identity across renders so consumers do not re-run effects', () => {
|
|
const { result, rerender } = renderHook(() => useUpdatePreferences());
|
|
const first = result.current;
|
|
rerender();
|
|
expect(result.current).toBe(first);
|
|
});
|
|
});
|
|
});
|