491 lines
12 KiB
TypeScript
491 lines
12 KiB
TypeScript
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(
|
|
<ToastComponent
|
|
toast={{ ...mockToast, type: 'error' }}
|
|
onDismiss={onDismiss}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
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();
|
|
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();
|
|
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();
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(onDismiss).toHaveBeenCalledWith('1');
|
|
},
|
|
{ timeout: 500 },
|
|
);
|
|
});
|
|
|
|
it('renders correct icon for each type', async () => {
|
|
const onDismiss = vi.fn();
|
|
const { container, rerender } = render(
|
|
<ToastComponent
|
|
toast={{ ...mockToast, type: 'success' }}
|
|
onDismiss={onDismiss}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
const icon = container.querySelector('svg');
|
|
expect(icon).toBeInTheDocument();
|
|
});
|
|
|
|
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(
|
|
<ToastComponent
|
|
toast={{ id: '1', message: 'Test' }}
|
|
onDismiss={onDismiss}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
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 />
|
|
</ToastProvider>,
|
|
);
|
|
});
|
|
|
|
it('throws error when used outside provider', () => {
|
|
const TestComponent = () => {
|
|
try {
|
|
useToastContext();
|
|
return <div>Should not render</div>;
|
|
} catch (error: any) {
|
|
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>
|
|
<button
|
|
onClick={() => addToast({ message: 'Test', type: 'success' })}
|
|
>
|
|
Add Toast
|
|
</button>
|
|
<div data-testid="toast-count">{toasts.length}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<TestComponent />
|
|
</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>
|
|
<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 />
|
|
</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 (
|
|
<button
|
|
onClick={() =>
|
|
addToast({ message: 'Test message', type: 'success' })
|
|
}
|
|
>
|
|
Add Toast
|
|
</button>
|
|
);
|
|
};
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<TestComponent />
|
|
</ToastProvider>,
|
|
);
|
|
|
|
const button = screen.getByText('Add Toast');
|
|
await act(async () => {
|
|
button.click();
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Test message')).toBeInTheDocument();
|
|
},
|
|
{ timeout: 1000 },
|
|
);
|
|
});
|
|
|
|
it('applies correct position classes', () => {
|
|
const { container, rerender } = render(
|
|
<ToastProvider position="top-right">
|
|
<div>Test</div>
|
|
</ToastProvider>,
|
|
);
|
|
|
|
let positionDiv = container.querySelector('.top-4.right-4');
|
|
expect(positionDiv).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<ToastProvider position="bottom-left">
|
|
<div>Test</div>
|
|
</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 />
|
|
</ToastProvider>,
|
|
);
|
|
});
|
|
|
|
it('displays success toast', async () => {
|
|
const TestComponent = () => {
|
|
const toast = useToast();
|
|
return (
|
|
<button onClick={() => toast.success('Success message')}>
|
|
Show Success
|
|
</button>
|
|
);
|
|
};
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<TestComponent />
|
|
</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();
|
|
},
|
|
{ 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 />
|
|
</ToastProvider>,
|
|
);
|
|
|
|
const button = screen.getByText('Show Error');
|
|
await act(async () => {
|
|
button.click();
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Error message')).toBeInTheDocument();
|
|
},
|
|
{ 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 />
|
|
</ToastProvider>,
|
|
);
|
|
|
|
const button = screen.getByText('Show Warning');
|
|
await act(async () => {
|
|
button.click();
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Warning message')).toBeInTheDocument();
|
|
},
|
|
{ timeout: 1000 },
|
|
);
|
|
});
|
|
|
|
it('displays info toast', async () => {
|
|
const TestComponent = () => {
|
|
const toast = useToast();
|
|
return (
|
|
<button onClick={() => toast.info('Info message')}>Show Info</button>
|
|
);
|
|
};
|
|
|
|
render(
|
|
<ToastProvider>
|
|
<TestComponent />
|
|
</ToastProvider>,
|
|
);
|
|
|
|
const button = screen.getByText('Show Info');
|
|
await act(async () => {
|
|
button.click();
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
},
|
|
{ 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 />
|
|
</ToastProvider>,
|
|
);
|
|
|
|
const button = screen.getByText('Show Custom');
|
|
await act(async () => {
|
|
button.click();
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(screen.getByText('Custom message')).toBeInTheDocument();
|
|
},
|
|
{ timeout: 1000 },
|
|
);
|
|
});
|
|
});
|
|
});
|