import type { ReactNode } from 'react' import type { ToastHandle } from '../index' import { act, render, screen, waitFor, within } from '@testing-library/react' import { noop } from 'es-toolkit/function' import * as React from 'react' import Toast, { ToastProvider } from '..' import { useToastContext } from '../context' const TestComponent = () => { const { notify, close } = useToastContext() return (
) } describe('Toast', () => { const getToastElementByMessage = (message: string): HTMLElement => { const messageElement = screen.getByText(message) const toastElement = messageElement.closest('.fixed') expect(toastElement).toBeInTheDocument() return toastElement as HTMLElement } beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }) }) afterEach(() => { vi.runOnlyPendingTimers() vi.useRealTimers() }) describe('Toast Component', () => { it('renders toast with correct type and message', () => { render( , ) expect(screen.getByText('Success message')).toBeInTheDocument() }) it('renders with different types', () => { const { rerender } = render( , ) const successToast = getToastElementByMessage('Success message') expect(successToast).toHaveClass('z-[1101]') const successIcon = within(successToast).getByTestId('toast-icon-success') expect(successIcon).toHaveClass('text-text-success') rerender( , ) const errorToast = getToastElementByMessage('Error message') const errorIcon = within(errorToast).getByTestId('toast-icon-error') expect(errorIcon).toHaveClass('text-text-destructive') }) it('renders with custom component', () => { render( Custom} /> , ) expect(screen.getByTestId('custom-component')).toBeInTheDocument() }) it('renders children content', () => { render( Additional information , ) expect(screen.getByText('Additional information')).toBeInTheDocument() }) it('does not render close button when close is undefined', () => { // Create a modified context where close is undefined const CustomToastContext = React.createContext({ notify: noop, close: undefined }) // Create a wrapper component using the custom context const Wrapper = ({ children }: { children: ReactNode }) => ( {children} ) render( , ) expect(screen.getByText('No close button')).toBeInTheDocument() const toastElement = getToastElementByMessage('No close button') expect(within(toastElement).queryByRole('button')).not.toBeInTheDocument() }) it('returns null when message is not a string', () => { const { container } = render( {/* @ts-expect-error - testing invalid input */} Invalid} /> , ) // Toast returns null, and provider adds no DOM elements expect(container.firstChild).toBeNull() }) it('renders with size sm', () => { const { rerender } = render( , ) const infoToast = getToastElementByMessage('Small size') const infoIcon = within(infoToast).getByTestId('toast-icon-info') expect(infoIcon).toHaveClass('text-text-accent', 'h-4', 'w-4') expect(infoIcon.parentElement).toHaveClass('p-1') rerender( , ) const successToast = getToastElementByMessage('Small size') const successIcon = within(successToast).getByTestId('toast-icon-success') expect(successIcon).toHaveClass('text-text-success', 'h-4', 'w-4') rerender( , ) const warningToast = getToastElementByMessage('Small size') const warningIcon = within(warningToast).getByTestId('toast-icon-warning') expect(warningIcon).toHaveClass('text-text-warning-secondary', 'h-4', 'w-4') rerender( , ) const errorToast = getToastElementByMessage('Small size') const errorIcon = within(errorToast).getByTestId('toast-icon-error') expect(errorIcon).toHaveClass('text-text-destructive', 'h-4', 'w-4') }) }) describe('ToastProvider and Context', () => { it('shows and hides toast using context', async () => { render( , ) // No toast initially expect(screen.queryByText('Notification message')).not.toBeInTheDocument() // Show toast act(() => { screen.getByText('Show Toast').click() }) expect(screen.getByText('Notification message')).toBeInTheDocument() // Close toast act(() => { screen.getByText('Close Toast').click() }) expect(screen.queryByText('Notification message')).not.toBeInTheDocument() }) it('automatically hides toast after duration', async () => { render( , ) // Show toast act(() => { screen.getByText('Show Toast').click() }) expect(screen.getByText('Notification message')).toBeInTheDocument() // Fast-forward timer act(() => { vi.advanceTimersByTime(3000) // Default for info type is 3000ms }) // Toast should be gone await waitFor(() => { expect(screen.queryByText('Notification message')).not.toBeInTheDocument() }) }) it('automatically hides toast after duration for error type in provider', async () => { const TestComponentError = () => { const { notify } = useToastContext() return ( ) } render( , ) act(() => { screen.getByText('Show Error').click() }) expect(screen.getByText('Error notify')).toBeInTheDocument() // Error type uses 6000ms default act(() => { vi.advanceTimersByTime(6000) }) await waitFor(() => { expect(screen.queryByText('Error notify')).not.toBeInTheDocument() }) }) }) describe('Toast.notify static method', () => { it('creates and removes toast from DOM', async () => { act(() => { // Call the static method Toast.notify({ message: 'Static notification', type: 'warning' }) }) // Toast should be in document expect(screen.getByText('Static notification')).toBeInTheDocument() // Fast-forward timer act(() => { vi.advanceTimersByTime(6000) // Default for warning type is 6000ms }) // Toast should be removed await waitFor(() => { expect(screen.queryByText('Static notification')).not.toBeInTheDocument() }) }) it('calls onClose callback after duration', async () => { const onCloseMock = vi.fn() act(() => { Toast.notify({ message: 'Closing notification', type: 'success', onClose: onCloseMock, }) }) // Fast-forward timer act(() => { vi.advanceTimersByTime(3000) // Default for success type is 3000ms }) // onClose should be called await waitFor(() => { expect(onCloseMock).toHaveBeenCalled() }) }) it('closes when close button is clicked in static toast', async () => { const onCloseMock = vi.fn() act(() => { Toast.notify({ message: 'Static close test', type: 'info', onClose: onCloseMock }) }) expect(screen.getByText('Static close test')).toBeInTheDocument() const toastElement = getToastElementByMessage('Static close test') const closeButton = within(toastElement).getByRole('button') act(() => { closeButton.click() }) expect(screen.queryByText('Static close test')).not.toBeInTheDocument() expect(onCloseMock).toHaveBeenCalled() }) it('does not auto close when duration is 0', async () => { act(() => { Toast.notify({ message: 'No auto close', type: 'info', duration: 0 }) }) expect(screen.getByText('No auto close')).toBeInTheDocument() act(() => { vi.advanceTimersByTime(10000) }) expect(screen.getByText('No auto close')).toBeInTheDocument() // manual clear to clean up act(() => { const toastElement = getToastElementByMessage('No auto close') within(toastElement).getByRole('button').click() }) }) it('returns a toast handler that can clear the toast', async () => { let handler: ToastHandle = {} const onCloseMock = vi.fn() act(() => { handler = Toast.notify({ message: 'Clearable toast', type: 'warning', onClose: onCloseMock }) }) expect(screen.getByText('Clearable toast')).toBeInTheDocument() act(() => { handler.clear?.() }) expect(screen.queryByText('Clearable toast')).not.toBeInTheDocument() expect(onCloseMock).toHaveBeenCalled() }) }) })