veza/apps/web/src/features/auth/hooks/useUsernameAvailability.test.ts
2025-12-12 21:34:34 -05:00

163 lines
4.5 KiB
TypeScript

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