veza/apps/web/src/components/feedback/Toast.test.tsx

492 lines
12 KiB
TypeScript
Raw Normal View History

import { render, screen, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ToastComponent, Toast } from './Toast';
import { ToastProvider, useToastContext } from './ToastProvider';
import { useToast } from '@/hooks/useToast';
describe('Toast Components', () => {
describe('ToastComponent', () => {
const mockToast: Toast = {
id: '1',
message: 'Test message',
type: 'success',
};
it('renders toast with message', async () => {
const onDismiss = vi.fn();
render(<ToastComponent toast={mockToast} onDismiss={onDismiss} />);
await waitFor(() => {
expect(screen.getByText('Test message')).toBeInTheDocument();
});
});
it('renders with correct type styling', async () => {
const onDismiss = vi.fn();
const { container } = render(
2025-12-13 02:34:34 +00:00
<ToastComponent
toast={{ ...mockToast, type: 'error' }}
onDismiss={onDismiss}
/>,
);
await waitFor(() => {
2025-12-13 02:34:34 +00:00
const toast = container.querySelector(
'.bg-red-50, .dark\\:bg-red-900\\/20',
);
expect(toast).toBeInTheDocument();
});
});
it('auto-dismisses after duration', async () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
2025-12-13 02:34:34 +00:00
render(
<ToastComponent
toast={{ ...mockToast, duration: 1000 }}
onDismiss={onDismiss}
/>,
);
await waitFor(() => {
expect(screen.getByText('Test message')).toBeInTheDocument();
});
act(() => {
vi.advanceTimersByTime(1100);
});
await waitFor(() => {
expect(onDismiss).toHaveBeenCalledWith('1');
});
vi.useRealTimers();
});
it('does not auto-dismiss when duration is 0', async () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
2025-12-13 02:34:34 +00:00
render(
<ToastComponent
toast={{ ...mockToast, duration: 0 }}
onDismiss={onDismiss}
/>,
);
await waitFor(() => {
expect(screen.getByText('Test message')).toBeInTheDocument();
});
act(() => {
vi.advanceTimersByTime(6000);
});
await waitFor(() => {
expect(onDismiss).not.toHaveBeenCalled();
});
vi.useRealTimers();
});
it('calls onDismiss when close button is clicked', async () => {
const onDismiss = vi.fn();
render(<ToastComponent toast={mockToast} onDismiss={onDismiss} />);
await waitFor(() => {
expect(screen.getByText('Test message')).toBeInTheDocument();
});
const closeButton = screen.getByLabelText('Fermer');
closeButton.click();
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(onDismiss).toHaveBeenCalledWith('1');
},
{ timeout: 500 },
);
});
it('renders correct icon for each type', async () => {
const onDismiss = vi.fn();
const { container, rerender } = render(
2025-12-13 02:34:34 +00:00
<ToastComponent
toast={{ ...mockToast, type: 'success' }}
onDismiss={onDismiss}
/>,
);
await waitFor(() => {
const icon = container.querySelector('svg');
expect(icon).toBeInTheDocument();
});
2025-12-13 02:34:34 +00:00
rerender(
<ToastComponent
toast={{ ...mockToast, type: 'error' }}
onDismiss={onDismiss}
/>,
);
rerender(
<ToastComponent
toast={{ ...mockToast, type: 'warning' }}
onDismiss={onDismiss}
/>,
);
rerender(
<ToastComponent
toast={{ ...mockToast, type: 'info' }}
onDismiss={onDismiss}
/>,
);
});
it('renders with default type when type is not provided', async () => {
const onDismiss = vi.fn();
const { container } = render(
2025-12-13 02:34:34 +00:00
<ToastComponent
toast={{ id: '1', message: 'Test' }}
onDismiss={onDismiss}
/>,
);
await waitFor(() => {
expect(screen.getByText('Test')).toBeInTheDocument();
2025-12-13 02:34:34 +00:00
const toast = container.querySelector(
'.bg-blue-50, .dark\\:bg-blue-900\\/20',
);
expect(toast).toBeInTheDocument();
});
});
});
describe('ToastProvider', () => {
it('provides toast context', () => {
const TestComponent = () => {
const context = useToastContext();
expect(context).toBeDefined();
expect(context.toasts).toEqual([]);
expect(typeof context.addToast).toBe('function');
expect(typeof context.removeToast).toBe('function');
return <div>Test</div>;
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
});
it('throws error when used outside provider', () => {
const TestComponent = () => {
try {
useToastContext();
return <div>Should not render</div>;
} catch (error: any) {
2025-12-13 02:34:34 +00:00
expect(error.message).toContain(
'useToastContext must be used within ToastProvider',
);
return <div>Error caught</div>;
}
};
render(<TestComponent />);
});
it('adds toast to queue', async () => {
const TestComponent = () => {
const { addToast, toasts } = useToastContext();
return (
<div>
2025-12-13 02:34:34 +00:00
<button
onClick={() => addToast({ message: 'Test', type: 'success' })}
>
Add Toast
</button>
<div data-testid="toast-count">{toasts.length}</div>
</div>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Add Toast');
button.click();
await waitFor(() => {
expect(screen.getByTestId('toast-count')).toHaveTextContent('1');
});
});
it('removes toast from queue', async () => {
const TestComponent = () => {
const { addToast, removeToast, toasts } = useToastContext();
return (
<div>
2025-12-13 02:34:34 +00:00
<button
onClick={() => addToast({ message: 'Test', type: 'success' })}
>
Add Toast
</button>
<button onClick={() => removeToast(toasts[0]?.id || '')}>
Remove Toast
</button>
<div data-testid="toast-count">{toasts.length}</div>
</div>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const addButton = screen.getByText('Add Toast');
addButton.click();
await waitFor(() => {
expect(screen.getByTestId('toast-count')).toHaveTextContent('1');
});
const removeButton = screen.getByText('Remove Toast');
removeButton.click();
await waitFor(() => {
expect(screen.getByTestId('toast-count')).toHaveTextContent('0');
});
});
it('renders toasts in container', async () => {
const TestComponent = () => {
const { addToast } = useToastContext();
return (
2025-12-13 02:34:34 +00:00
<button
onClick={() =>
addToast({ message: 'Test message', type: 'success' })
}
>
Add Toast
</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Add Toast');
await act(async () => {
button.click();
});
await waitFor(
() => {
expect(screen.getByText('Test message')).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
it('applies correct position classes', () => {
const { container, rerender } = render(
<ToastProvider position="top-right">
<div>Test</div>
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
let positionDiv = container.querySelector('.top-4.right-4');
expect(positionDiv).toBeInTheDocument();
rerender(
<ToastProvider position="bottom-left">
<div>Test</div>
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
positionDiv = container.querySelector('.bottom-4.left-4');
expect(positionDiv).toBeInTheDocument();
});
});
describe('useToast hook', () => {
it('provides toast methods', () => {
const TestComponent = () => {
const toast = useToast();
expect(typeof toast.success).toBe('function');
expect(typeof toast.error).toBe('function');
expect(typeof toast.warning).toBe('function');
expect(typeof toast.info).toBe('function');
expect(typeof toast.toast).toBe('function');
return <div>Test</div>;
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
});
it('displays success toast', async () => {
const TestComponent = () => {
const toast = useToast();
return (
<button onClick={() => toast.success('Success message')}>
Show Success
</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Show Success');
await act(async () => {
button.click();
});
// Wait for toast to appear (with animation delay)
await waitFor(
() => {
const toast = screen.queryByText('Success message');
expect(toast).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
it('displays error toast', async () => {
const TestComponent = () => {
const toast = useToast();
return (
<button onClick={() => toast.error('Error message')}>
Show Error
</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Show Error');
await act(async () => {
button.click();
});
await waitFor(
() => {
expect(screen.getByText('Error message')).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
it('displays warning toast', async () => {
const TestComponent = () => {
const toast = useToast();
return (
<button onClick={() => toast.warning('Warning message')}>
Show Warning
</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Show Warning');
await act(async () => {
button.click();
});
await waitFor(
() => {
expect(screen.getByText('Warning message')).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
it('displays info toast', async () => {
const TestComponent = () => {
const toast = useToast();
return (
2025-12-13 02:34:34 +00:00
<button onClick={() => toast.info('Info message')}>Show Info</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Show Info');
await act(async () => {
button.click();
});
await waitFor(
() => {
expect(screen.getByText('Info message')).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
it('allows custom toast with toast method', async () => {
const TestComponent = () => {
const toast = useToast();
return (
<button
onClick={() =>
toast.toast({
message: 'Custom message',
type: 'success',
duration: 2000,
})
}
>
Show Custom
</button>
);
};
render(
<ToastProvider>
<TestComponent />
2025-12-13 02:34:34 +00:00
</ToastProvider>,
);
const button = screen.getByText('Show Custom');
await act(async () => {
button.click();
});
await waitFor(
() => {
expect(screen.getByText('Custom message')).toBeInTheDocument();
},
2025-12-13 02:34:34 +00:00
{ timeout: 1000 },
);
});
});
});