94 lines
3.5 KiB
React
94 lines
3.5 KiB
React
|
|
// @vitest-environment jsdom
|
||
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { renderHook, act } from '@testing-library/react';
|
||
|
|
import { useAutoSave } from './useAutoSave';
|
||
|
|
|
||
|
|
describe('useAutoSave', () => {
|
||
|
|
beforeEach(() => vi.useFakeTimers());
|
||
|
|
afterEach(() => vi.useRealTimers());
|
||
|
|
|
||
|
|
it('debounces: only the latest payload is saved, once', async () => {
|
||
|
|
const save = vi.fn().mockResolvedValue();
|
||
|
|
const { result } = renderHook(() => useAutoSave(save, 400));
|
||
|
|
|
||
|
|
act(() => {
|
||
|
|
result.current.schedule({ a: 1 });
|
||
|
|
result.current.schedule({ a: 2 });
|
||
|
|
result.current.schedule({ a: 3 });
|
||
|
|
});
|
||
|
|
expect(save).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(400); });
|
||
|
|
expect(save).toHaveBeenCalledTimes(1);
|
||
|
|
expect(save).toHaveBeenCalledWith({ a: 3 });
|
||
|
|
expect(result.current.status).toBe('saved');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('per-call delay overrides the default', async () => {
|
||
|
|
const save = vi.fn().mockResolvedValue();
|
||
|
|
const { result } = renderHook(() => useAutoSave(save, 400));
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ slow: true }, 900); });
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(400); });
|
||
|
|
expect(save).not.toHaveBeenCalled();
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(500); });
|
||
|
|
expect(save).toHaveBeenCalledWith({ slow: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('flush() saves a pending payload immediately and is a no-op when idle', async () => {
|
||
|
|
const save = vi.fn().mockResolvedValue();
|
||
|
|
const { result } = renderHook(() => useAutoSave(save, 400));
|
||
|
|
|
||
|
|
await act(async () => { result.current.flush(); });
|
||
|
|
expect(save).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ b: 1 }, 900); });
|
||
|
|
await act(async () => { result.current.flush(); });
|
||
|
|
expect(save).toHaveBeenCalledTimes(1);
|
||
|
|
expect(save).toHaveBeenCalledWith({ b: 1 });
|
||
|
|
|
||
|
|
// payload no longer pending — flushing again must not double-save
|
||
|
|
await act(async () => { result.current.flush(); });
|
||
|
|
expect(save).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports error status when the save rejects, then recovers on next save', async () => {
|
||
|
|
const save = vi.fn()
|
||
|
|
.mockRejectedValueOnce(new Error('boom'))
|
||
|
|
.mockResolvedValueOnce();
|
||
|
|
const { result } = renderHook(() => useAutoSave(save, 100));
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ x: 1 }); });
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(100); });
|
||
|
|
expect(result.current.status).toBe('error');
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ x: 2 }); });
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(100); });
|
||
|
|
expect(result.current.status).toBe('saved');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('"saved" fades back to idle after 2 seconds', async () => {
|
||
|
|
const save = vi.fn().mockResolvedValue();
|
||
|
|
const { result } = renderHook(() => useAutoSave(save, 100));
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ y: 1 }); });
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(100); });
|
||
|
|
expect(result.current.status).toBe('saved');
|
||
|
|
|
||
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(2000); });
|
||
|
|
expect(result.current.status).toBe('idle');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('flushes a pending payload on unmount so edits are never lost', async () => {
|
||
|
|
const save = vi.fn().mockResolvedValue();
|
||
|
|
const { result, unmount } = renderHook(() => useAutoSave(save, 900));
|
||
|
|
|
||
|
|
act(() => { result.current.schedule({ unsaved: true }); });
|
||
|
|
expect(save).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
unmount();
|
||
|
|
expect(save).toHaveBeenCalledTimes(1);
|
||
|
|
expect(save).toHaveBeenCalledWith({ unsaved: true });
|
||
|
|
});
|
||
|
|
});
|