Files
smoothschedule/frontend/src/components/__tests__/Portal.test.tsx
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:36:46 -05:00

454 lines
13 KiB
TypeScript

/**
* 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>
<div data-testid="portal-content">Portal Content</div>
</Portal>
);
expect(screen.getByTestId('portal-content')).toBeInTheDocument();
expect(screen.getByText('Portal Content')).toBeInTheDocument();
});
it('should render text content', () => {
render(<Portal>Simple text content</Portal>);
expect(screen.getByText('Simple text content')).toBeInTheDocument();
});
it('should render complex JSX children', () => {
render(
<Portal>
<div>
<h1>Title</h1>
<p>Description</p>
<button>Click me</button>
</div>
</Portal>
);
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(
<div id="root">
<Portal>
<div data-testid="portal-content">Portal Content</div>
</Portal>
</div>
);
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(
<div id="parent" style={{ position: 'relative', zIndex: 1 }}>
<div id="child">
<Portal>
<div data-testid="portal-content">Escaped Content</div>
</Portal>
</div>
</div>
);
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(
<Portal>
<div data-testid="child-1">First child</div>
<div data-testid="child-2">Second child</div>
<div data-testid="child-3">Third child</div>
</Portal>
);
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(
<Portal>
{items.map((item, index) => (
<div key={index} data-testid={`item-${index}`}>
{item}
</div>
))}
</Portal>
);
items.forEach((item, index) => {
expect(screen.getByTestId(`item-${index}`)).toBeInTheDocument();
expect(screen.getByText(item)).toBeInTheDocument();
});
});
it('should render nested components', () => {
const NestedComponent = () => (
<div data-testid="nested">
<span>Nested Component</span>
</div>
);
render(
<Portal>
<NestedComponent />
<div>Other content</div>
</Portal>
);
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(
<Portal>
<div data-testid="portal-content">Content</div>
</Portal>
);
// After initial render, content should be present
expect(screen.getByTestId('portal-content')).toBeInTheDocument();
// Re-render should still show content
rerender(
<Portal>
<div data-testid="portal-content">Updated Content</div>
</Portal>
);
expect(screen.getByText('Updated Content')).toBeInTheDocument();
});
});
describe('Multiple Portals', () => {
it('should support multiple portal instances', () => {
render(
<div>
<Portal>
<div data-testid="portal-1">Portal 1</div>
</Portal>
<Portal>
<div data-testid="portal-2">Portal 2</div>
</Portal>
<Portal>
<div data-testid="portal-3">Portal 3</div>
</Portal>
</div>
);
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(
<div>
<Portal>
<div data-testid="portal-1">
<span data-testid="content-1">Content 1</span>
</div>
</Portal>
<Portal>
<div data-testid="portal-2">
<span data-testid="content-2">Content 2</span>
</div>
</Portal>
</div>
);
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(
<Portal>
<div data-testid="portal-content">Temporary Content</div>
</Portal>
);
// 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(
<div>
<Portal>
<div data-testid="portal-1">Portal 1</div>
</Portal>
<Portal>
<div data-testid="portal-2">Portal 2</div>
</Portal>
</div>
);
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(
<Portal>
<div data-testid="portal-content">Initial Content</div>
</Portal>
);
expect(screen.getByText('Initial Content')).toBeInTheDocument();
rerender(
<Portal>
<div data-testid="portal-content">Updated Content</div>
</Portal>
);
expect(screen.getByText('Updated Content')).toBeInTheDocument();
expect(screen.queryByText('Initial Content')).not.toBeInTheDocument();
});
it('should handle prop changes', () => {
const TestComponent = ({ message }: { message: string }) => (
<Portal>
<div data-testid="message">{message}</div>
</Portal>
);
const { rerender } = render(<TestComponent message="First message" />);
expect(screen.getByText('First message')).toBeInTheDocument();
rerender(<TestComponent message="Second message" />);
expect(screen.getByText('Second message')).toBeInTheDocument();
expect(screen.queryByText('First message')).not.toBeInTheDocument();
});
});
describe('Edge Cases', () => {
it('should handle empty children', () => {
render(<Portal>{null}</Portal>);
// Should not throw error
expect(document.body).toBeInTheDocument();
});
it('should handle undefined children', () => {
render(<Portal>{undefined}</Portal>);
// Should not throw error
expect(document.body).toBeInTheDocument();
});
it('should handle boolean children', () => {
render(
<Portal>
{false && <div>Should not render</div>}
{true && <div data-testid="should-render">Should render</div>}
</Portal>
);
expect(screen.queryByText('Should not render')).not.toBeInTheDocument();
expect(screen.getByTestId('should-render')).toBeInTheDocument();
});
it('should handle conditional rendering', () => {
const { rerender } = render(
<Portal>
{false && <div data-testid="conditional">Conditional Content</div>}
</Portal>
);
expect(screen.queryByTestId('conditional')).not.toBeInTheDocument();
rerender(
<Portal>
{true && <div data-testid="conditional">Conditional Content</div>}
</Portal>
);
expect(screen.getByTestId('conditional')).toBeInTheDocument();
});
});
describe('Integration with Parent Components', () => {
it('should work inside modals', () => {
const Modal = ({ children }: { children: React.ReactNode }) => (
<div className="modal" data-testid="modal">
<Portal>{children}</Portal>
</div>
);
const { container } = render(
<Modal>
<div data-testid="modal-content">Modal Content</div>
</Modal>
);
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(
<Portal>
<button data-testid="button" onClick={handleClick}>
Click me
</button>
</Portal>
);
const button = screen.getByTestId('button');
button.click();
expect(clicked).toBe(true);
});
it('should preserve CSS classes and styles', () => {
render(
<Portal>
<div
data-testid="styled-content"
className="custom-class"
style={{ color: 'red', fontSize: '16px' }}
>
Styled Content
</div>
</Portal>
);
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(
<Portal>
<div
data-testid="aria-content"
role="dialog"
aria-label="Test Dialog"
aria-describedby="description"
>
<div id="description">Dialog description</div>
</div>
</Portal>
);
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(
<Portal>
<dialog open data-testid="dialog">
<h2>Dialog Title</h2>
<p>Dialog content</p>
</dialog>
</Portal>
);
expect(screen.getByTestId('dialog')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Dialog Title' })).toBeInTheDocument();
});
});
});