import { render, screen, fireEvent } from '@testing-library/react'
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
import { ResponsiveDrawer, resolveOrigins } from './responsive-drawer'
import { ThemeProvider } from '@mui/material'
import { createTheme } from '@mui/material/styles'
// Create a basic theme for testing
const theme = createTheme()
describe('ResponsiveDrawer', () => {
const mockOnChangeCollapsed = vi.fn()
// A real anchor element: the desktop Popover only opens when `ref != null`
// (the open-by-default guard), and positioning needs a DOM node.
let anchor: HTMLElement
const defaultProps = {
collapsed: false,
onChangeCollapsed: mockOnChangeCollapsed,
position: 'top-left' as const,
isMobile: false,
children:
Test Content
,
}
beforeEach(() => {
mockOnChangeCollapsed.mockReset()
anchor = document.createElement('div')
document.body.appendChild(anchor)
})
afterEach(() => {
anchor.remove()
})
test('renders in desktop mode when isMobile is false', () => {
const { container } = render(
,
)
// Check if content is rendered
const content = screen.getByTestId('drawer-content')
expect(content).toBeDefined()
// Make sure we're not using the mobile Drawer (no presentation role)
const presentationElements = container.querySelectorAll(
'[role="presentation"]',
)
expect(presentationElements.length).toBe(0)
})
test('does not open the desktop popover until the anchor ref resolves', () => {
// Open-by-default guard: collapsed=false but ref=null → stays closed so the
// popover never renders with a null anchorEl.
render(
,
)
expect(screen.queryByTestId('drawer-content')).toBeNull()
})
test('renders in mobile mode when isMobile is true', () => {
render(
,
)
// Check if content is rendered
const content = screen.getByTestId('drawer-content')
expect(content).toBeDefined()
// Mobile mode uses MUI Drawer which has a 'presentation' role
const presentationElements = document.querySelectorAll(
'[role="presentation"]',
)
expect(presentationElements.length).toBeGreaterThan(0)
})
test('hides content when collapsed is true in desktop mode', () => {
render(
,
)
// When collapsed, the Popover should be closed and content should not be in the document
const content = screen.queryByTestId('drawer-content')
expect(content).toBeNull()
})
test('closes drawer when clicking outside in mobile mode', () => {
const { container } = render(
,
)
// Find and click the backdrop to close the drawer
const backdrop = container.querySelector('[role="presentation"] > div')
expect(backdrop).toBeDefined()
if (backdrop) {
fireEvent.click(backdrop)
// onChangeCollapsed should be called to close the drawer
expect(mockOnChangeCollapsed).toHaveBeenCalledTimes(1)
expect(mockOnChangeCollapsed).toHaveBeenCalledWith(true)
}
})
// Each of the four `position` values produces a different
// anchorOrigin/transformOrigin combination.
test.each(['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const)(
'renders in desktop mode with position=%s',
(position) => {
expect(() =>
render(
,
),
).not.toThrow()
},
)
test('modal (default): renders an (invisible) backdrop', () => {
render(
,
)
// The popover portals to document.body, so query the whole document.
expect(document.querySelector('.MuiBackdrop-root')).not.toBeNull()
})
test('forwards slotProps to the underlying Popover', () => {
const slotProps = { paper: { 'data-testid': 'paper-slot' } } as never
expect(() =>
render(
,
),
).not.toThrow()
})
test('defaults to portaling into document.body when container is omitted', () => {
render(
,
)
const content = screen.getByTestId('drawer-content')
const modalRoot = content.closest('.MuiModal-root')
expect(modalRoot?.parentElement).toBe(document.body)
})
test('portals into the given container instead of document.body', () => {
const customContainer = document.createElement('div')
document.body.appendChild(customContainer)
try {
render(
,
)
const content = screen.getByTestId('drawer-content')
const modalRoot = content.closest('.MuiModal-root')
expect(modalRoot?.parentElement).toBe(customContainer)
} finally {
customContainer.remove()
}
})
test('caps the popover paper height at 90% of a measured container element', () => {
const customContainer = document.createElement('div')
Object.defineProperty(customContainer, 'clientHeight', {
value: 500,
configurable: true,
})
document.body.appendChild(customContainer)
try {
render(
,
)
const paper = document.querySelector('.MuiPopover-paper')!
expect(paper.style.maxHeight).toBe('450px')
expect(paper.style.overflow).toBe('hidden')
expect(paper.style.display).toBe('flex')
expect(paper.style.flexDirection).toBe('column')
const content = screen.getByTestId('drawer-content')
const wrapper = content.parentElement!
expect(wrapper.style.display).toBe('flex')
expect(wrapper.style.flexDirection).toBe('column')
expect(wrapper.style.flex).toBe('1 1 0%')
expect(wrapper.style.minHeight).toBe('0')
expect(wrapper.style.maxHeight).toBe('100%')
expect(wrapper.style.overflow).toBe('hidden')
} finally {
customContainer.remove()
}
})
test('leaves paper maxHeight unset when no container is given', () => {
render(
,
)
const paper = document.querySelector('.MuiPopover-paper')!
expect(paper.style.maxHeight).toBe('')
expect(paper.style.display).toBe('')
const content = screen.getByTestId('drawer-content')
expect(content.parentElement!.style.display).toBe('')
})
test('leaves paper maxHeight unset when the container reports height 0', () => {
const customContainer = document.createElement('div')
Object.defineProperty(customContainer, 'clientHeight', {
value: 0,
configurable: true,
})
document.body.appendChild(customContainer)
try {
render(
,
)
const paper = document.querySelector('.MuiPopover-paper')!
expect(paper.style.maxHeight).toBe('')
} finally {
customContainer.remove()
}
})
test('preserves slotProps.root when capping paper height under modal=true', () => {
const customContainer = document.createElement('div')
Object.defineProperty(customContainer, 'clientHeight', {
value: 500,
configurable: true,
})
document.body.appendChild(customContainer)
try {
render(
,
)
expect(screen.getByTestId('drawer-root-slot')).toBeDefined()
expect(screen.getByTestId('drawer-paper-slot')).toBeDefined()
const paper = document.querySelector('.MuiPopover-paper')!
expect(paper.style.maxHeight).toBe('450px')
} finally {
customContainer.remove()
}
})
test('respects sx prop on the Popover', () => {
expect(() =>
render(
,
),
).not.toThrow()
})
// ── Non-modal mode (modal={false}) ─────────────────────────────────────────
test('non-modal: renders no backdrop', () => {
render(
,
)
expect(document.querySelector('.MuiBackdrop-root')).toBeNull()
// Content still renders.
expect(screen.getByTestId('drawer-content')).toBeDefined()
})
test('non-modal: Escape does not close the panel', () => {
render(
,
)
// Fire on a node inside the Modal root so it reaches the Modal's key handler.
fireEvent.keyDown(screen.getByTestId('drawer-content'), {
key: 'Escape',
code: 'Escape',
})
expect(mockOnChangeCollapsed).not.toHaveBeenCalled()
})
test('modal (default): Escape closes the panel', () => {
render(
,
)
fireEvent.keyDown(screen.getByTestId('drawer-content'), {
key: 'Escape',
code: 'Escape',
})
expect(mockOnChangeCollapsed).toHaveBeenCalledWith(true)
})
})
// ── resolveOrigins (pure) ─────────────────────────────────────────────────────
describe('resolveOrigins', () => {
test('side placement opens beside the trigger (horizontal flip)', () => {
expect(resolveOrigins('bottom-right', 'side')).toEqual({
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformOrigin: { vertical: 'bottom', horizontal: 'right' },
})
expect(resolveOrigins('top-left', 'side')).toEqual({
anchorOrigin: { vertical: 'top', horizontal: 'right' },
transformOrigin: { vertical: 'top', horizontal: 'left' },
})
})
test('overlay placement pins corner-to-corner (panel sits on top of the anchor)', () => {
// anchorOrigin === transformOrigin for every corner → the panel lands on the
// anchor's own corner (it's hidden while open).
for (const position of [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
] as const) {
const { anchorOrigin, transformOrigin } = resolveOrigins(
position,
'overlay',
)
expect(anchorOrigin).toEqual(transformOrigin)
}
expect(resolveOrigins('bottom-right', 'overlay').anchorOrigin).toEqual({
vertical: 'bottom',
horizontal: 'right',
})
expect(resolveOrigins('top-left', 'overlay').anchorOrigin).toEqual({
vertical: 'top',
horizontal: 'left',
})
})
test('transformOrigin is independent of placement (panel keeps its own corner)', () => {
for (const position of [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
] as const) {
const side = resolveOrigins(position, 'side')
const overlay = resolveOrigins(position, 'overlay')
expect(overlay.transformOrigin).toEqual(side.transformOrigin)
}
})
})