import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor, act } from '@testing-library/react'; import { useUsernameAvailability } from './useUsernameAvailability'; import { checkUsernameAvailability } from '../services/authService'; vi.mock('../services/authService', () => ({ checkUsernameAvailability: vi.fn(), })); describe('useUsernameAvailability', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); it('should return null when username is empty', () => { const { result } = renderHook(() => useUsernameAvailability('')); expect(result.current.available).toBeNull(); expect(result.current.checking).toBe(false); }); it('should return null when username is too short', () => { const { result } = renderHook(() => useUsernameAvailability('ab')); expect(result.current.available).toBeNull(); expect(result.current.checking).toBe(false); }); it('should check availability when username is long enough', async () => { vi.mocked(checkUsernameAvailability).mockResolvedValue(true); const { result } = renderHook(() => useUsernameAvailability('testuser')); // Wait for debounce (500ms) and API call await waitFor( () => { expect(checkUsernameAvailability).toHaveBeenCalledWith('testuser'); }, { timeout: 2000 }, ); await waitFor( () => { expect(result.current.available).toBe(true); expect(result.current.checking).toBe(false); }, { timeout: 2000 }, ); }); it('should return false when username is not available', async () => { vi.mocked(checkUsernameAvailability).mockResolvedValue(false); const { result } = renderHook(() => useUsernameAvailability('takenuser')); // Wait for debounce (500ms) and API call await waitFor( () => { expect(checkUsernameAvailability).toHaveBeenCalledWith('takenuser'); }, { timeout: 2000 }, ); await waitFor( () => { expect(result.current.available).toBe(false); expect(result.current.checking).toBe(false); }, { timeout: 2000 }, ); }); it('should debounce the API call', async () => { vi.mocked(checkUsernameAvailability).mockResolvedValue(true); const { result, rerender } = renderHook( ({ username }) => useUsernameAvailability(username), { initialProps: { username: 'test' } }, ); // Change username multiple times quickly await act(async () => { rerender({ username: 'test1' }); await new Promise((resolve) => setTimeout(resolve, 100)); rerender({ username: 'test12' }); await new Promise((resolve) => setTimeout(resolve, 100)); rerender({ username: 'test123' }); }); // Wait for debounce to complete and API call await waitFor( () => { expect(checkUsernameAvailability).toHaveBeenCalled(); }, { timeout: 2000 }, ); await waitFor( () => { expect(result.current.available).toBe(true); }, { timeout: 2000 }, ); // Should only be called once for the last value expect(checkUsernameAvailability).toHaveBeenCalledTimes(1); expect(checkUsernameAvailability).toHaveBeenCalledWith('test123'); }); it('should handle errors gracefully', async () => { vi.mocked(checkUsernameAvailability).mockRejectedValue( new Error('API Error'), ); const { result } = renderHook(() => useUsernameAvailability('testuser')); // Wait for debounce (500ms) and API call await waitFor( () => { expect(checkUsernameAvailability).toHaveBeenCalledWith('testuser'); }, { timeout: 2000 }, ); // Wait for promise to reject and state to update await waitFor( () => { expect(result.current.available).toBeNull(); expect(result.current.checking).toBe(false); }, { timeout: 2000 }, ); }); it('should reset when username changes to empty', async () => { vi.mocked(checkUsernameAvailability).mockResolvedValue(true); const { result, rerender } = renderHook( ({ username }) => useUsernameAvailability(username), { initialProps: { username: 'testuser' } }, ); // Wait for debounce and API call await waitFor( () => { expect(result.current.available).toBe(true); }, { timeout: 2000 }, ); // Change to empty await act(async () => { rerender({ username: '' }); }); expect(result.current.available).toBeNull(); expect(result.current.checking).toBe(false); }); });