veza/apps/web/src/features/auth/hooks/useUsernameAvailability.test.ts

136 lines
4.3 KiB
TypeScript
Raw Normal View History

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