import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Alert from './alert';
describe('Alert', () => {
describe('Rendering', () => {
it('should render alert with title and message', () => {
render(
Test message
);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test message')).toBeInTheDocument();
});
it('should not render when open is false', () => {
render(
Test message
);
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('should render all severity levels correctly', () => {
const severities = ['default', 'info', 'success', 'warning', 'error'] as const;
severities.forEach((severity) => {
const { unmount } = render(
{severity} message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-alert', severity);
unmount();
});
});
it('should hide icon when hideIcon is true', () => {
const { container } = render(
Test message
);
expect(container.querySelector('.alert-icon')).not.toBeInTheDocument();
});
it('should show icon by default', () => {
const { container } = render(
Test message
);
expect(container.querySelector('.alert-icon')).toBeInTheDocument();
});
it('should render with custom icon size', () => {
const { container } = render(
Test message
);
const icon = container.querySelector('.alert-icon svg');
expect(icon).toBeInTheDocument();
// Note: Actual size verification would require checking the SVG element's attributes
// which depends on the Icon component implementation
});
it('should use default icon size of 24px when iconSize not specified', () => {
const { container } = render(
Test message
);
const iconContainer = container.querySelector('.alert-icon');
expect(iconContainer).toBeInTheDocument();
});
it('should render actions when provided', () => {
render(
>
}
>
Test message
);
expect(screen.getByText('Undo')).toBeInTheDocument();
expect(screen.getByText('Dismiss')).toBeInTheDocument();
});
it('should render dismiss button when dismissible is true', () => {
render(
Test message
);
expect(screen.getByRole('button', { name: /close alert/i })).toBeInTheDocument();
});
it('should apply correct variant attribute', () => {
const variants = ['outlined', 'filled', 'soft'] as const;
variants.forEach((variant) => {
const { unmount } = render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-variant', variant);
unmount();
});
});
});
describe('Interactions', () => {
it('should call onDismiss when dismiss button is clicked', async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
Test message
);
const dismissButton = screen.getByRole('button', { name: /close alert/i });
await user.click(dismissButton);
// Wait for animation timeout (300ms)
await waitFor(() => {
expect(onDismiss).toHaveBeenCalledTimes(1);
}, { timeout: 500 });
});
it('should dismiss alert when ESC key is pressed', async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
Test message
);
await user.keyboard('{Escape}');
// Wait for animation timeout
await waitFor(() => {
expect(onDismiss).toHaveBeenCalledTimes(1);
}, { timeout: 500 });
});
it('should not dismiss with ESC key when not dismissible', async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
Test message
);
await user.keyboard('{Escape}');
// Wait a bit to ensure it doesn't dismiss
await new Promise(resolve => setTimeout(resolve, 100));
expect(onDismiss).not.toHaveBeenCalled();
});
it('should set data-visible to false when dismissing', async () => {
const user = userEvent.setup();
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-visible', 'true');
const dismissButton = screen.getByRole('button', { name: /close alert/i });
await user.click(dismissButton);
// Check that data-visible changes immediately
expect(alert).toHaveAttribute('data-visible', 'false');
});
});
describe('Auto-dismiss', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('should auto-dismiss after specified duration', async () => {
const onDismiss = vi.fn();
render(
Auto-dismiss message
);
// Fast-forward time by 3000ms (auto-hide duration)
act(() => {
vi.advanceTimersByTime(3000);
});
// Fast-forward by 300ms for animation
await act(async () => {
vi.advanceTimersByTime(300);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('should not auto-dismiss when autoHideDuration is 0', async () => {
const onDismiss = vi.fn();
render(
No auto-dismiss
);
vi.advanceTimersByTime(5000);
expect(onDismiss).not.toHaveBeenCalled();
});
it('should not auto-dismiss when autoHideDuration is undefined', async () => {
const onDismiss = vi.fn();
render(
No auto-dismiss
);
vi.advanceTimersByTime(5000);
expect(onDismiss).not.toHaveBeenCalled();
});
});
describe('Accessibility', () => {
it('should have correct aria-live for error severity', () => {
render(
Error message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('aria-live', 'assertive');
});
it('should have correct aria-live for non-error severities', () => {
const severities = ['default', 'info', 'success', 'warning'] as const;
severities.forEach((severity) => {
const { unmount } = render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('aria-live', 'polite');
unmount();
});
});
it('should have aria-atomic attribute', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('aria-atomic', 'true');
});
it('should have role="alert"', () => {
render(
Test message
);
expect(screen.getByRole('alert')).toBeInTheDocument();
});
it('should focus alert when autoFocus is true', () => {
render(
Critical alert
);
const alert = screen.getByRole('alert');
expect(alert).toHaveFocus();
});
it('should not focus alert when autoFocus is false', () => {
render(
Normal alert
);
const alert = screen.getByRole('alert');
expect(alert).not.toHaveFocus();
});
it('should have tabIndex=-1 when autoFocus is true', () => {
render(
Critical alert
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('tabIndex', '-1');
});
});
describe('Animation State', () => {
it('should start with data-visible=true when open=true', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-visible', 'true');
});
it('should transition visibility states when dismissing', async () => {
const user = userEvent.setup();
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-visible', 'true');
const dismissButton = screen.getByRole('button', { name: /close alert/i });
await user.click(dismissButton);
// After click, visibility should be false but component still rendered
expect(alert).toHaveAttribute('data-visible', 'false');
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
describe('Phase 4: WCAG 2.1 Accessibility', () => {
describe('Severity Text for Screen Readers', () => {
it('should include visually hidden severity text for info', () => {
const { container } = render(
Test message
);
const srOnlyText = container.querySelector('.sr-only');
expect(srOnlyText).toBeInTheDocument();
expect(srOnlyText).toHaveTextContent('Information:');
});
it('should include visually hidden severity text for success', () => {
const { container } = render(
Test message
);
const srOnlyText = container.querySelector('.sr-only');
expect(srOnlyText).toHaveTextContent('Success:');
});
it('should include visually hidden severity text for warning', () => {
const { container } = render(
Test message
);
const srOnlyText = container.querySelector('.sr-only');
expect(srOnlyText).toHaveTextContent('Warning:');
});
it('should include visually hidden severity text for error', () => {
const { container } = render(
Test message
);
const srOnlyText = container.querySelector('.sr-only');
expect(srOnlyText).toHaveTextContent('Error:');
});
it('should not include severity text for default severity', () => {
const { container } = render(
Test message
);
const srOnlyText = container.querySelector('.sr-only');
expect(srOnlyText).not.toBeInTheDocument();
});
});
describe('Configurable Heading Level', () => {
it('should render h2 when titleLevel is 2', () => {
const { container } = render(
Test message
);
const heading = container.querySelector('h2.alert-title');
expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent('Test Title');
});
it('should render h3 when titleLevel is 3', () => {
const { container } = render(
Test message
);
const heading = container.querySelector('h3.alert-title');
expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent('Test Title');
});
it('should render h4 when titleLevel is 4', () => {
const { container } = render(
Test message
);
const heading = container.querySelector('h4.alert-title');
expect(heading).toBeInTheDocument();
});
it('should render h5 when titleLevel is 5', () => {
const { container } = render(
Test message
);
const heading = container.querySelector('h5.alert-title');
expect(heading).toBeInTheDocument();
});
it('should render h6 when titleLevel is 6', () => {
const { container } = render(
Test message
);
const heading = container.querySelector('h6.alert-title');
expect(heading).toBeInTheDocument();
});
it('should render default heading element when titleLevel is undefined', () => {
const { container } = render(
Test message
);
// The UI component wraps the title, check that title is rendered
const titleElement = container.querySelector('.alert-title');
expect(titleElement).toBeInTheDocument();
expect(titleElement).toHaveTextContent('Test Title');
// The element should be rendered (exact tag depends on UI component implementation)
});
it('should not render title element when title prop is not provided', () => {
const { container } = render(
Test message
);
expect(container.querySelector('.alert-title')).not.toBeInTheDocument();
});
it('should apply alert-title class to all heading levels', () => {
const levels = [2, 3, 4, 5, 6] as const;
levels.forEach((level) => {
const { container, unmount } = render(
Message
);
const heading = container.querySelector(`h${level}`);
expect(heading).toHaveClass('alert-title');
unmount();
});
});
});
describe('Pause on Hover/Focus', () => {
it('should have mouse enter and leave handlers when pauseOnHover is true', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
// Verify the handlers are attached by checking we can dispatch events without errors
expect(() => {
alert.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
alert.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
}).not.toThrow();
});
it('should have focus and blur handlers when pauseOnHover is true', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
// Verify the handlers are attached
expect(() => {
alert.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
alert.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
}).not.toThrow();
});
it('should accept pauseOnHover prop with default true', () => {
const { rerender } = render(
Test message
);
// Just verify component renders without issues
expect(screen.getByRole('alert')).toBeInTheDocument();
rerender(
Test message
);
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
describe('Touch Target Size', () => {
it('should apply alert-dismiss class to dismiss button', () => {
const { container } = render(
Test message
);
const dismissButton = container.querySelector('.alert-dismiss');
expect(dismissButton).toBeInTheDocument();
});
});
describe('Focus Indicators', () => {
it('should be focusable when autoFocus is true', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('tabIndex', '-1');
});
it('should not have tabIndex when autoFocus is false', () => {
render(
Test message
);
const alert = screen.getByRole('alert');
expect(alert).not.toHaveAttribute('tabIndex');
});
});
describe('Content Type', () => {
it('should wrap children in paragraph tag when contentType is "text" (default)', () => {
const { container } = render(
Simple text content
);
const paragraph = container.querySelector('.alert-message p');
expect(paragraph).toBeInTheDocument();
expect(paragraph).toHaveTextContent('Simple text content');
});
it('should wrap children in paragraph tag when contentType is explicitly set to "text"', () => {
const { container } = render(
Explicit text content
);
const paragraph = container.querySelector('.alert-message p');
expect(paragraph).toBeInTheDocument();
expect(paragraph).toHaveTextContent('Explicit text content');
});
it('should render children directly without paragraph wrapper when contentType is "node"', () => {
const { container } = render(
Custom layout
);
// Should not have a paragraph wrapper
const paragraph = container.querySelector('.alert-message > p');
expect(paragraph).not.toBeInTheDocument();
// Should have direct custom content
const customContent = container.querySelector('.alert-message .custom-content');
expect(customContent).toBeInTheDocument();
expect(customContent).toHaveTextContent('Custom layout');
});
it('should render complex content with lists when contentType is "node"', () => {
const { container } = render(
First item
Second item
Third item
);
const list = container.querySelector('.alert-message ul');
expect(list).toBeInTheDocument();
const listItems = container.querySelectorAll('.alert-message li');
expect(listItems).toHaveLength(3);
expect(listItems[0]).toHaveTextContent('First item');
expect(listItems[1]).toHaveTextContent('Second item');
expect(listItems[2]).toHaveTextContent('Third item');
});
it('should render multiple child elements when contentType is "node"', () => {
const { container } = render(