Implemented `requestAnimationFrame` throttling for the scroll event listener in `Navbar` to reduce the frequency of state updates and logic execution. - Wrapped scroll handler in `requestAnimationFrame`. - Added performance test `src/components/layout/Navbar.test.tsx` verifying logic runs once per frame instead of per event. - Verified functional correctness of scroll state updates. - Fixed `useTypingEffect` test environment.
111 lines
2.9 KiB
TypeScript
111 lines
2.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { renderHook, act } from '@testing-library/react';
|
|
import { useTypingEffect } from './useTypingEffect';
|
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
|
|
describe('useTypingEffect', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should type text, pause, and delete', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTypingEffect({
|
|
words: ['Hello'],
|
|
typingSpeed: 10,
|
|
deletingSpeed: 10,
|
|
pauseDuration: 100,
|
|
})
|
|
);
|
|
|
|
// Initial state
|
|
expect(result.current.text).toBe('');
|
|
|
|
// Advance time incrementally to ensure state updates and effects run
|
|
// "Hello" is 5 chars. Should take roughly 50-60ms.
|
|
// We loop more than enough times to ensure it completes.
|
|
for (let i = 0; i < 10; i++) {
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(11);
|
|
});
|
|
if (result.current.text === 'Hello') break;
|
|
}
|
|
|
|
expect(result.current.text).toBe('Hello');
|
|
|
|
// Should be in pause state (isTyping false, isDeleting false)
|
|
// Wait for pause duration (100ms)
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(50);
|
|
});
|
|
// Check if paused
|
|
expect(result.current.isTyping).toBe(false);
|
|
expect(result.current.isDeleting).toBe(false);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(60);
|
|
});
|
|
|
|
// Should be deleting now
|
|
expect(result.current.isDeleting).toBe(true);
|
|
|
|
// Advance to delete "Hello"
|
|
for (let i = 0; i < 10; i++) {
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(11);
|
|
});
|
|
if (result.current.text === '') break;
|
|
}
|
|
|
|
expect(result.current.text).toBe('');
|
|
expect(result.current.isDeleting).toBe(false);
|
|
});
|
|
|
|
it('should clean up timers on unmount', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const { result, unmount } = renderHook(() =>
|
|
useTypingEffect({
|
|
words: ['Hi'],
|
|
typingSpeed: 10,
|
|
deletingSpeed: 10,
|
|
pauseDuration: 100,
|
|
})
|
|
);
|
|
|
|
// Type "Hi"
|
|
for (let i = 0; i < 10; i++) {
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(11);
|
|
});
|
|
if (result.current.text === 'Hi') break;
|
|
}
|
|
expect(result.current.text).toBe('Hi');
|
|
|
|
// Trigger the pause logic
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(20);
|
|
});
|
|
|
|
// Unmount the component
|
|
unmount();
|
|
|
|
// Advance time past pause where the timeout would fire
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(200);
|
|
});
|
|
|
|
// We verify no console errors regarding unmounted updates
|
|
const unmountErrors = consoleSpy.mock.calls.filter(args =>
|
|
args[0] && typeof args[0] === 'string' &&
|
|
(args[0].includes('unmounted component') || args[0].includes('state update'))
|
|
);
|
|
|
|
expect(unmountErrors).toHaveLength(0);
|
|
});
|
|
});
|