Files
ragusaitweb/src/hooks/useTypingEffect.test.ts
google-labs-jules[bot] 8592775485 perf: throttle scroll event listener in Navbar
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.
2026-01-22 08:24:10 +00:00

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);
});
});