import React, { useRef, useState } from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import MagicPortal from 'react-magic-portal'
describe('MagicPortal', () => {
beforeEach(() => {
document.body.innerHTML = ''
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Basic Functionality', () => {
it('should render children in portal when anchor exists', () => {
// Setup anchor element
const anchor = document.createElement('div')
anchor.id = 'test-anchor'
document.body.appendChild(anchor)
render(
Portal Content
)
const portalContent = screen.getByTestId('portal-content')
expect(portalContent).toBeTruthy()
expect(anchor.contains(portalContent)).toBe(true)
})
it('should not render children when anchor does not exist', () => {
render(
Portal Content
)
expect(screen.queryByTestId('portal-content')).toBeNull()
})
it('should render children when anchor appears later', async () => {
function TestComponent() {
const [showAnchor, setShowAnchor] = useState(false)
return (
{showAnchor &&
Anchor
}
Portal Content
)
}
const user = userEvent.setup()
render()
expect(screen.queryByTestId('portal-content')).toBeNull()
await user.click(screen.getByText('Show Anchor'))
await waitFor(() => {
expect(screen.getByTestId('portal-content')).toBeTruthy()
})
})
it('should clean up when anchor is removed', async () => {
function TestComponent() {
const [showAnchor, setShowAnchor] = useState(true)
return (
)
const anchor = document.getElementById('position-anchor')!
const portalContent = screen.getByTestId('portal-content')
expect(anchor.contains(portalContent)).toBe(true)
})
it('should first when position is first', () => {
render(
Portal Content
)
const anchor = document.getElementById('position-anchor')!
const portalContent = screen.getByTestId('portal-content')
expect(anchor.contains(portalContent)).toBe(true)
})
it('should position before when position is before', () => {
render(
Portal Content
)
const anchor = document.getElementById('position-anchor')!
const portalContent = screen.getByTestId('portal-content')
expect(portalContent).toBeTruthy()
// For before position, content should be positioned before the anchor
expect(anchor.parentElement!.contains(portalContent) || document.body.contains(portalContent)).toBe(true)
})
it('should position after when position is after', () => {
render(
Portal Content
)
const anchor = document.getElementById('position-anchor')!
const portalContent = screen.getByTestId('portal-content')
expect(portalContent).toBeTruthy()
// For after position, content should be positioned after the anchor
expect(anchor.parentElement!.contains(portalContent) || document.body.contains(portalContent)).toBe(true)
})
})
describe('Lifecycle Callbacks', () => {
it('should call onMount when portal is mounted', async () => {
const onMount = vi.fn()
const anchor = document.createElement('div')
anchor.id = 'mount-anchor'
document.body.appendChild(anchor)
render(
)
}).not.toThrow()
expect(screen.queryByTestId('portal-content')).toBeNull()
})
it('should handle function that returns null', () => {
render(
null}>
Portal Content
)
expect(screen.queryByTestId('portal-content')).toBeNull()
})
it('should handle function that throws error', () => {
const errorFunction = () => {
throw new Error('Test error')
}
// The component should handle the error gracefully and not crash
expect(() => {
render(
Portal Content
)
}).toThrow('Test error')
// Portal content should not be rendered when anchor function throws
expect(screen.queryByTestId('portal-content')).toBeNull()
})
})
describe('Key Prop', () => {
it('should pass key to ReactDOM.createPortal', () => {
const anchor = document.createElement('div')
anchor.id = 'key-anchor'
document.body.appendChild(anchor)
const { rerender } = render(
>
)
// Should log error about Fragment not being supported
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[react-magic-portal] Fragment children are not supported, expected to receive a single React element child.'
)
// Anchor should remain empty since Fragment is not supported
expect(anchor.textContent).toBe('')
expect(anchor.children.length).toBe(0)
consoleErrorSpy.mockRestore()
})
it('should not render pure text content', () => {
const anchor = document.createElement('div')
anchor.id = 'text-only-anchor'
document.body.appendChild(anchor)
render(
// @ts-expect-error - testing that text nodes are not rendered
Just plain text
)
// Anchor should remain empty since text nodes are not rendered
expect(anchor.textContent).toBe('')
expect(anchor.children.length).toBe(0)
})
it('should not render null children', () => {
const anchor = document.createElement('div')
anchor.id = 'null-anchor'
document.body.appendChild(anchor)
render({null})
// Anchor should remain empty since null is not rendered
expect(anchor.textContent).toBe('')
expect(anchor.children.length).toBe(0)
})
})
describe('Cleanup', () => {
it('should clean up MutationObserver on unmount', () => {
const anchor = document.createElement('div')
anchor.id = 'cleanup-anchor'
document.body.appendChild(anchor)
const { unmount } = render(