/** * Unit tests for Portal component * * Tests the Portal component which uses ReactDOM.createPortal to render * children outside the parent DOM hierarchy. This is useful for modals, * tooltips, and other UI elements that need to escape parent stacking contexts. */ import { describe, it, expect, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import Portal from '../Portal'; describe('Portal', () => { afterEach(() => { // Clean up any rendered components cleanup(); }); describe('Basic Rendering', () => { it('should render children', () => { render(
Portal Content
); expect(screen.getByTestId('portal-content')).toBeInTheDocument(); expect(screen.getByText('Portal Content')).toBeInTheDocument(); }); it('should render text content', () => { render(Simple text content); expect(screen.getByText('Simple text content')).toBeInTheDocument(); }); it('should render complex JSX children', () => { render(

Title

Description

); expect(screen.getByRole('heading', { name: 'Title' })).toBeInTheDocument(); expect(screen.getByText('Description')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); }); }); describe('Portal Behavior', () => { it('should render content to document.body', () => { const { container } = render(
Portal Content
); const portalContent = screen.getByTestId('portal-content'); // Portal content should NOT be inside the container expect(container.contains(portalContent)).toBe(false); // Portal content SHOULD be inside document.body expect(document.body.contains(portalContent)).toBe(true); }); it('should escape parent DOM hierarchy', () => { const { container } = render(
Escaped Content
); const portalContent = screen.getByTestId('portal-content'); const parent = container.querySelector('#parent'); // Portal content should not be inside parent expect(parent?.contains(portalContent)).toBe(false); // Portal content should be direct child of body expect(portalContent.parentElement).toBe(document.body); }); }); describe('Multiple Children', () => { it('should render multiple children', () => { render(
First child
Second child
Third child
); expect(screen.getByTestId('child-1')).toBeInTheDocument(); expect(screen.getByTestId('child-2')).toBeInTheDocument(); expect(screen.getByTestId('child-3')).toBeInTheDocument(); }); it('should render an array of children', () => { const items = ['Item 1', 'Item 2', 'Item 3']; render( {items.map((item, index) => (
{item}
))}
); items.forEach((item, index) => { expect(screen.getByTestId(`item-${index}`)).toBeInTheDocument(); expect(screen.getByText(item)).toBeInTheDocument(); }); }); it('should render nested components', () => { const NestedComponent = () => (
Nested Component
); render(
Other content
); expect(screen.getByTestId('nested')).toBeInTheDocument(); expect(screen.getByText('Nested Component')).toBeInTheDocument(); expect(screen.getByText('Other content')).toBeInTheDocument(); }); }); describe('Mounting Behavior', () => { it('should not render before component is mounted', () => { // This test verifies the internal mounting state const { rerender } = render(
Content
); // After initial render, content should be present expect(screen.getByTestId('portal-content')).toBeInTheDocument(); // Re-render should still show content rerender(
Updated Content
); expect(screen.getByText('Updated Content')).toBeInTheDocument(); }); }); describe('Multiple Portals', () => { it('should support multiple portal instances', () => { render(
Portal 1
Portal 2
Portal 3
); expect(screen.getByTestId('portal-1')).toBeInTheDocument(); expect(screen.getByTestId('portal-2')).toBeInTheDocument(); expect(screen.getByTestId('portal-3')).toBeInTheDocument(); // All portals should be in document.body expect(document.body.contains(screen.getByTestId('portal-1'))).toBe(true); expect(document.body.contains(screen.getByTestId('portal-2'))).toBe(true); expect(document.body.contains(screen.getByTestId('portal-3'))).toBe(true); }); it('should keep portals separate from each other', () => { render(
Content 1
Content 2
); const portal1 = screen.getByTestId('portal-1'); const portal2 = screen.getByTestId('portal-2'); const content1 = screen.getByTestId('content-1'); const content2 = screen.getByTestId('content-2'); // Each portal should contain only its own content expect(portal1.contains(content1)).toBe(true); expect(portal1.contains(content2)).toBe(false); expect(portal2.contains(content2)).toBe(true); expect(portal2.contains(content1)).toBe(false); }); }); describe('Cleanup', () => { it('should remove content from body when unmounted', () => { const { unmount } = render(
Temporary Content
); // Content should exist initially expect(screen.getByTestId('portal-content')).toBeInTheDocument(); // Unmount the component unmount(); // Content should be removed from DOM expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument(); }); it('should clean up multiple portals on unmount', () => { const { unmount } = render(
Portal 1
Portal 2
); expect(screen.getByTestId('portal-1')).toBeInTheDocument(); expect(screen.getByTestId('portal-2')).toBeInTheDocument(); unmount(); expect(screen.queryByTestId('portal-1')).not.toBeInTheDocument(); expect(screen.queryByTestId('portal-2')).not.toBeInTheDocument(); }); }); describe('Re-rendering', () => { it('should update content on re-render', () => { const { rerender } = render(
Initial Content
); expect(screen.getByText('Initial Content')).toBeInTheDocument(); rerender(
Updated Content
); expect(screen.getByText('Updated Content')).toBeInTheDocument(); expect(screen.queryByText('Initial Content')).not.toBeInTheDocument(); }); it('should handle prop changes', () => { const TestComponent = ({ message }: { message: string }) => (
{message}
); const { rerender } = render(); expect(screen.getByText('First message')).toBeInTheDocument(); rerender(); expect(screen.getByText('Second message')).toBeInTheDocument(); expect(screen.queryByText('First message')).not.toBeInTheDocument(); }); }); describe('Edge Cases', () => { it('should handle empty children', () => { render({null}); // Should not throw error expect(document.body).toBeInTheDocument(); }); it('should handle undefined children', () => { render({undefined}); // Should not throw error expect(document.body).toBeInTheDocument(); }); it('should handle boolean children', () => { render( {false &&
Should not render
} {true &&
Should render
}
); expect(screen.queryByText('Should not render')).not.toBeInTheDocument(); expect(screen.getByTestId('should-render')).toBeInTheDocument(); }); it('should handle conditional rendering', () => { const { rerender } = render( {false &&
Conditional Content
}
); expect(screen.queryByTestId('conditional')).not.toBeInTheDocument(); rerender( {true &&
Conditional Content
}
); expect(screen.getByTestId('conditional')).toBeInTheDocument(); }); }); describe('Integration with Parent Components', () => { it('should work inside modals', () => { const Modal = ({ children }: { children: React.ReactNode }) => (
{children}
); const { container } = render(
Modal Content
); const modalContent = screen.getByTestId('modal-content'); const modal = container.querySelector('[data-testid="modal"]'); // Modal content should not be inside modal container expect(modal?.contains(modalContent)).toBe(false); // Modal content should be in document.body expect(document.body.contains(modalContent)).toBe(true); }); it('should preserve event handlers', () => { let clicked = false; const handleClick = () => { clicked = true; }; render( ); const button = screen.getByTestId('button'); button.click(); expect(clicked).toBe(true); }); it('should preserve CSS classes and styles', () => { render(
Styled Content
); const styledContent = screen.getByTestId('styled-content'); expect(styledContent).toHaveClass('custom-class'); // Check styles individually - color may be normalized to rgb() expect(styledContent.style.color).toBeTruthy(); expect(styledContent.style.fontSize).toBe('16px'); }); }); describe('Accessibility', () => { it('should maintain ARIA attributes', () => { render(
Dialog description
); const content = screen.getByTestId('aria-content'); expect(content).toHaveAttribute('role', 'dialog'); expect(content).toHaveAttribute('aria-label', 'Test Dialog'); expect(content).toHaveAttribute('aria-describedby', 'description'); }); it('should support semantic HTML inside portal', () => { render(

Dialog Title

Dialog content

); expect(screen.getByTestId('dialog')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Dialog Title' })).toBeInTheDocument(); }); }); });