a003f1192e
- 字体加载优化: 添加 font-display: block 策略,创建 useFontLoading hook - 色彩对比度: 调整 text-muted 和 text-tertiary 颜色值确保 WCAG AA 合规 - 滚动进度条: 新增 ScrollProgress 组件,支持 reduced motion - 表单自动保存: 新增 useFormAutosave hook,防止用户数据丢失 - 返回顶部按钮: 新增 BackToTop 组件,提升长页面导航体验 - 图片懒加载: 优化 OptimizedImage 组件,添加 blur placeholder 和加载动画 所有新组件均包含完整测试,1450+ 测试通过
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
import { useFormAutosave } from './use-form-autosave';
|
|
|
|
describe('useFormAutosave', () => {
|
|
const mockKey = 'test_form';
|
|
const mockInitialData = { name: '', email: '' };
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
jest.clearAllMocks();
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('should initialize with initial data', () => {
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
})
|
|
);
|
|
|
|
expect(result.current.data).toEqual(mockInitialData);
|
|
expect(result.current.lastSaved).toBeNull();
|
|
expect(result.current.isRestored).toBe(false);
|
|
});
|
|
|
|
it('should restore data from localStorage', () => {
|
|
const savedData = {
|
|
data: { name: 'John', email: 'john@example.com' },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
localStorage.setItem(`form_autosave_${mockKey}`, JSON.stringify(savedData));
|
|
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
})
|
|
);
|
|
|
|
expect(result.current.data).toEqual(savedData.data);
|
|
expect(result.current.isRestored).toBe(true);
|
|
});
|
|
|
|
it('should not restore expired data', () => {
|
|
const expiredData = {
|
|
data: { name: 'John', email: 'john@example.com' },
|
|
timestamp: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), // 25 hours ago
|
|
};
|
|
localStorage.setItem(`form_autosave_${mockKey}`, JSON.stringify(expiredData));
|
|
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
})
|
|
);
|
|
|
|
expect(result.current.data).toEqual(mockInitialData);
|
|
expect(result.current.isRestored).toBe(false);
|
|
});
|
|
|
|
it('should update data and auto-save after debounce', async () => {
|
|
const onSave = jest.fn();
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
onSave,
|
|
debounceMs: 1000,
|
|
})
|
|
);
|
|
|
|
act(() => {
|
|
result.current.updateData({ name: 'John' });
|
|
});
|
|
|
|
expect(result.current.data.name).toBe('John');
|
|
expect(result.current.lastSaved).toBeNull();
|
|
|
|
act(() => {
|
|
jest.advanceTimersByTime(1000);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.lastSaved).not.toBeNull();
|
|
});
|
|
|
|
expect(onSave).toHaveBeenCalledWith({ name: 'John', email: '' });
|
|
});
|
|
|
|
it('should clear saved data', () => {
|
|
const savedData = {
|
|
data: { name: 'John', email: 'john@example.com' },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
localStorage.setItem(`form_autosave_${mockKey}`, JSON.stringify(savedData));
|
|
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
})
|
|
);
|
|
|
|
act(() => {
|
|
result.current.clearSavedData();
|
|
});
|
|
|
|
expect(result.current.data).toEqual(mockInitialData);
|
|
expect(result.current.lastSaved).toBeNull();
|
|
expect(result.current.isRestored).toBe(false);
|
|
expect(localStorage.getItem(`form_autosave_${mockKey}`)).toBeNull();
|
|
});
|
|
|
|
it('should save immediately when called', async () => {
|
|
const onSave = jest.fn();
|
|
const { result } = renderHook(() =>
|
|
useFormAutosave({
|
|
key: mockKey,
|
|
initialData: mockInitialData,
|
|
onSave,
|
|
debounceMs: 5000,
|
|
})
|
|
);
|
|
|
|
act(() => {
|
|
result.current.updateData({ name: 'John' });
|
|
});
|
|
|
|
// 等待状态更新
|
|
await waitFor(() => {
|
|
expect(result.current.data.name).toBe('John');
|
|
});
|
|
|
|
act(() => {
|
|
result.current.saveImmediately();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(onSave).toHaveBeenCalledWith({ name: 'John', email: '' });
|
|
});
|
|
expect(result.current.lastSaved).not.toBeNull();
|
|
});
|
|
});
|