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