676 lines
19 KiB
TypeScript
676 lines
19 KiB
TypeScript
import React from 'react'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import Drawer from './index'
|
|
import type { IDrawerProps } from './index'
|
|
|
|
// Capture dialog onClose for testing
|
|
let capturedDialogOnClose: (() => void) | null = null
|
|
|
|
// Mock react-i18next
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
// Mock @headlessui/react
|
|
jest.mock('@headlessui/react', () => ({
|
|
Dialog: ({ children, open, onClose, className, unmount }: {
|
|
children: React.ReactNode
|
|
open: boolean
|
|
onClose: () => void
|
|
className: string
|
|
unmount: boolean
|
|
}) => {
|
|
capturedDialogOnClose = onClose
|
|
if (!open)
|
|
return null
|
|
return (
|
|
<div
|
|
data-testid="dialog"
|
|
data-open={open}
|
|
data-unmount={unmount}
|
|
className={className}
|
|
role="dialog"
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
},
|
|
DialogBackdrop: ({ children, className, onClick }: {
|
|
children?: React.ReactNode
|
|
className: string
|
|
onClick: () => void
|
|
}) => (
|
|
<div
|
|
data-testid="dialog-backdrop"
|
|
className={className}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</div>
|
|
),
|
|
DialogTitle: ({ children, as: _as, className, ...props }: {
|
|
children: React.ReactNode
|
|
as?: string
|
|
className?: string
|
|
}) => (
|
|
<div data-testid="dialog-title" className={className} {...props}>
|
|
{children}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Mock XMarkIcon
|
|
jest.mock('@heroicons/react/24/outline', () => ({
|
|
XMarkIcon: ({ className, onClick }: { className: string; onClick?: () => void }) => (
|
|
<svg data-testid="close-icon" className={className} onClick={onClick} />
|
|
),
|
|
}))
|
|
|
|
// Helper function to render Drawer with default props
|
|
const defaultProps: IDrawerProps = {
|
|
isOpen: true,
|
|
onClose: jest.fn(),
|
|
children: <div data-testid="drawer-content">Content</div>,
|
|
}
|
|
|
|
const renderDrawer = (props: Partial<IDrawerProps> = {}) => {
|
|
const mergedProps = { ...defaultProps, ...props }
|
|
return render(<Drawer {...mergedProps} />)
|
|
}
|
|
|
|
describe('Drawer', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
capturedDialogOnClose = null
|
|
})
|
|
|
|
// Basic rendering tests
|
|
describe('Rendering', () => {
|
|
it('should render when isOpen is true', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ isOpen: true })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
expect(screen.getByTestId('drawer-content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render when isOpen is false', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ isOpen: false })
|
|
|
|
// Assert
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render children content', () => {
|
|
// Arrange
|
|
const childContent = <p data-testid="custom-child">Custom Content</p>
|
|
|
|
// Act
|
|
renderDrawer({ children: childContent })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('custom-child')).toBeInTheDocument()
|
|
expect(screen.getByText('Custom Content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Title and description tests
|
|
describe('Title and Description', () => {
|
|
it('should render title when provided', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ title: 'Test Title' })
|
|
|
|
// Assert
|
|
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render title when not provided', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ title: '' })
|
|
|
|
// Assert
|
|
const titles = screen.queryAllByTestId('dialog-title')
|
|
const titleWithText = titles.find(el => el.textContent !== '')
|
|
expect(titleWithText).toBeUndefined()
|
|
})
|
|
|
|
it('should render description when provided', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ description: 'Test Description' })
|
|
|
|
// Assert
|
|
expect(screen.getByText('Test Description')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render description when not provided', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ description: '' })
|
|
|
|
// Assert
|
|
expect(screen.queryByText('Test Description')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render both title and description together', () => {
|
|
// Arrange & Act
|
|
renderDrawer({
|
|
title: 'My Title',
|
|
description: 'My Description',
|
|
})
|
|
|
|
// Assert
|
|
expect(screen.getByText('My Title')).toBeInTheDocument()
|
|
expect(screen.getByText('My Description')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Close button tests
|
|
describe('Close Button', () => {
|
|
it('should render close icon when showClose is true', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ showClose: true })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render close icon when showClose is false', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ showClose: false })
|
|
|
|
// Assert
|
|
expect(screen.queryByTestId('close-icon')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render close icon by default', () => {
|
|
// Arrange & Act
|
|
renderDrawer({})
|
|
|
|
// Assert
|
|
expect(screen.queryByTestId('close-icon')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onClose when close icon is clicked', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ showClose: true, onClose })
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('close-icon'))
|
|
|
|
// Assert
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
// Backdrop/Mask tests
|
|
describe('Backdrop and Mask', () => {
|
|
it('should render backdrop when noOverlay is false', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ noOverlay: false })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('dialog-backdrop')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render backdrop when noOverlay is true', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ noOverlay: true })
|
|
|
|
// Assert
|
|
expect(screen.queryByTestId('dialog-backdrop')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply mask background when mask is true', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ mask: true })
|
|
|
|
// Assert
|
|
const backdrop = screen.getByTestId('dialog-backdrop')
|
|
expect(backdrop.className).toContain('bg-black/30')
|
|
})
|
|
|
|
it('should not apply mask background when mask is false', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ mask: false })
|
|
|
|
// Assert
|
|
const backdrop = screen.getByTestId('dialog-backdrop')
|
|
expect(backdrop.className).not.toContain('bg-black/30')
|
|
})
|
|
|
|
it('should call onClose when backdrop is clicked and clickOutsideNotOpen is false', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ onClose, clickOutsideNotOpen: false })
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('dialog-backdrop'))
|
|
|
|
// Assert
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should not call onClose when backdrop is clicked and clickOutsideNotOpen is true', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ onClose, clickOutsideNotOpen: true })
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('dialog-backdrop'))
|
|
|
|
// Assert
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// Footer tests
|
|
describe('Footer', () => {
|
|
it('should render default footer with cancel and save buttons when footer is undefined', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ footer: undefined })
|
|
|
|
// Assert
|
|
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
|
expect(screen.getByText('common.operation.save')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render footer when footer is null', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ footer: null })
|
|
|
|
// Assert
|
|
expect(screen.queryByText('common.operation.cancel')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('common.operation.save')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render custom footer when provided', () => {
|
|
// Arrange
|
|
const customFooter = <div data-testid="custom-footer">Custom Footer</div>
|
|
|
|
// Act
|
|
renderDrawer({ footer: customFooter })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('custom-footer')).toBeInTheDocument()
|
|
expect(screen.queryByText('common.operation.cancel')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onCancel when cancel button is clicked', () => {
|
|
// Arrange
|
|
const onCancel = jest.fn()
|
|
renderDrawer({ onCancel })
|
|
|
|
// Act
|
|
const cancelButton = screen.getByText('common.operation.cancel')
|
|
fireEvent.click(cancelButton)
|
|
|
|
// Assert
|
|
expect(onCancel).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onOk when save button is clicked', () => {
|
|
// Arrange
|
|
const onOk = jest.fn()
|
|
renderDrawer({ onOk })
|
|
|
|
// Act
|
|
const saveButton = screen.getByText('common.operation.save')
|
|
fireEvent.click(saveButton)
|
|
|
|
// Assert
|
|
expect(onOk).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should not throw when onCancel is not provided and cancel is clicked', () => {
|
|
// Arrange
|
|
renderDrawer({ onCancel: undefined })
|
|
|
|
// Act & Assert
|
|
expect(() => {
|
|
fireEvent.click(screen.getByText('common.operation.cancel'))
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it('should not throw when onOk is not provided and save is clicked', () => {
|
|
// Arrange
|
|
renderDrawer({ onOk: undefined })
|
|
|
|
// Act & Assert
|
|
expect(() => {
|
|
fireEvent.click(screen.getByText('common.operation.save'))
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
// Custom className tests
|
|
describe('Custom ClassNames', () => {
|
|
it('should apply custom dialogClassName', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ dialogClassName: 'custom-dialog-class' })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog').className).toContain('custom-dialog-class')
|
|
})
|
|
|
|
it('should apply custom dialogBackdropClassName', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ dialogBackdropClassName: 'custom-backdrop-class' })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('dialog-backdrop').className).toContain('custom-backdrop-class')
|
|
})
|
|
|
|
it('should apply custom containerClassName', () => {
|
|
// Arrange & Act
|
|
const { container } = renderDrawer({ containerClassName: 'custom-container-class' })
|
|
|
|
// Assert
|
|
const containerDiv = container.querySelector('.custom-container-class')
|
|
expect(containerDiv).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply custom panelClassName', () => {
|
|
// Arrange & Act
|
|
const { container } = renderDrawer({ panelClassName: 'custom-panel-class' })
|
|
|
|
// Assert
|
|
const panelDiv = container.querySelector('.custom-panel-class')
|
|
expect(panelDiv).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Position tests
|
|
describe('Position', () => {
|
|
it('should apply center position class when positionCenter is true', () => {
|
|
// Arrange & Act
|
|
const { container } = renderDrawer({ positionCenter: true })
|
|
|
|
// Assert
|
|
const containerDiv = container.querySelector('.\\!justify-center')
|
|
expect(containerDiv).toBeInTheDocument()
|
|
})
|
|
|
|
it('should use end position by default when positionCenter is false', () => {
|
|
// Arrange & Act
|
|
const { container } = renderDrawer({ positionCenter: false })
|
|
|
|
// Assert
|
|
const containerDiv = container.querySelector('.justify-end')
|
|
expect(containerDiv).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Unmount prop tests
|
|
describe('Unmount Prop', () => {
|
|
it('should pass unmount prop to Dialog component', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ unmount: true })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('dialog').getAttribute('data-unmount')).toBe('true')
|
|
})
|
|
|
|
it('should default unmount to false', () => {
|
|
// Arrange & Act
|
|
renderDrawer({})
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('dialog').getAttribute('data-unmount')).toBe('false')
|
|
})
|
|
})
|
|
|
|
// Edge cases
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty string title', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ title: '' })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty string description', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ description: '' })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle special characters in title', () => {
|
|
// Arrange
|
|
const specialTitle = '<script>alert("xss")</script>'
|
|
|
|
// Act
|
|
renderDrawer({ title: specialTitle })
|
|
|
|
// Assert
|
|
expect(screen.getByText(specialTitle)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle very long title', () => {
|
|
// Arrange
|
|
const longTitle = 'A'.repeat(500)
|
|
|
|
// Act
|
|
renderDrawer({ title: longTitle })
|
|
|
|
// Assert
|
|
expect(screen.getByText(longTitle)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle complex children with multiple elements', () => {
|
|
// Arrange
|
|
const complexChildren = (
|
|
<div data-testid="complex-children">
|
|
<h1>Heading</h1>
|
|
<p>Paragraph</p>
|
|
<input data-testid="input-element" />
|
|
<button data-testid="button-element">Button</button>
|
|
</div>
|
|
)
|
|
|
|
// Act
|
|
renderDrawer({ children: complexChildren })
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('complex-children')).toBeInTheDocument()
|
|
expect(screen.getByText('Heading')).toBeInTheDocument()
|
|
expect(screen.getByText('Paragraph')).toBeInTheDocument()
|
|
expect(screen.getByTestId('input-element')).toBeInTheDocument()
|
|
expect(screen.getByTestId('button-element')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle null children gracefully', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ children: null as unknown as React.ReactNode })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle undefined footer without crashing', () => {
|
|
// Arrange & Act
|
|
renderDrawer({ footer: undefined })
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle rapid open/close toggles', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
const { rerender } = render(
|
|
<Drawer {...defaultProps} isOpen={true} onClose={onClose}>
|
|
<div>Content</div>
|
|
</Drawer>,
|
|
)
|
|
|
|
// Act - Toggle multiple times
|
|
rerender(
|
|
<Drawer {...defaultProps} isOpen={false} onClose={onClose}>
|
|
<div>Content</div>
|
|
</Drawer>,
|
|
)
|
|
rerender(
|
|
<Drawer {...defaultProps} isOpen={true} onClose={onClose}>
|
|
<div>Content</div>
|
|
</Drawer>,
|
|
)
|
|
rerender(
|
|
<Drawer {...defaultProps} isOpen={false} onClose={onClose}>
|
|
<div>Content</div>
|
|
</Drawer>,
|
|
)
|
|
|
|
// Assert
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Combined prop scenarios
|
|
describe('Combined Prop Scenarios', () => {
|
|
it('should render with all optional props', () => {
|
|
// Arrange & Act
|
|
renderDrawer({
|
|
title: 'Full Feature Title',
|
|
description: 'Full Feature Description',
|
|
dialogClassName: 'custom-dialog',
|
|
dialogBackdropClassName: 'custom-backdrop',
|
|
containerClassName: 'custom-container',
|
|
panelClassName: 'custom-panel',
|
|
showClose: true,
|
|
mask: true,
|
|
positionCenter: true,
|
|
unmount: true,
|
|
noOverlay: false,
|
|
footer: <div data-testid="custom-full-footer">Footer</div>,
|
|
})
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
expect(screen.getByText('Full Feature Title')).toBeInTheDocument()
|
|
expect(screen.getByText('Full Feature Description')).toBeInTheDocument()
|
|
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
|
expect(screen.getByTestId('custom-full-footer')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render minimal drawer with only required props', () => {
|
|
// Arrange
|
|
const minimalProps: IDrawerProps = {
|
|
isOpen: true,
|
|
onClose: jest.fn(),
|
|
children: <div>Minimal Content</div>,
|
|
}
|
|
|
|
// Act
|
|
render(<Drawer {...minimalProps} />)
|
|
|
|
// Assert
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
expect(screen.getByText('Minimal Content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle showClose with title simultaneously', () => {
|
|
// Arrange & Act
|
|
renderDrawer({
|
|
title: 'Title with Close',
|
|
showClose: true,
|
|
})
|
|
|
|
// Assert
|
|
expect(screen.getByText('Title with Close')).toBeInTheDocument()
|
|
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle noOverlay with clickOutsideNotOpen', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
|
|
// Act
|
|
renderDrawer({
|
|
noOverlay: true,
|
|
clickOutsideNotOpen: true,
|
|
onClose,
|
|
})
|
|
|
|
// Assert - backdrop should not exist
|
|
expect(screen.queryByTestId('dialog-backdrop')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// Dialog onClose callback tests (e.g., Escape key)
|
|
describe('Dialog onClose Callback', () => {
|
|
it('should call onClose when Dialog triggers close and clickOutsideNotOpen is false', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ onClose, clickOutsideNotOpen: false })
|
|
|
|
// Act - Simulate Dialog's onClose (e.g., pressing Escape)
|
|
capturedDialogOnClose?.()
|
|
|
|
// Assert
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should not call onClose when Dialog triggers close and clickOutsideNotOpen is true', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ onClose, clickOutsideNotOpen: true })
|
|
|
|
// Act - Simulate Dialog's onClose (e.g., pressing Escape)
|
|
capturedDialogOnClose?.()
|
|
|
|
// Assert
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onClose by default when Dialog triggers close', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ onClose })
|
|
|
|
// Act
|
|
capturedDialogOnClose?.()
|
|
|
|
// Assert
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
// Event handler interaction tests
|
|
describe('Event Handler Interactions', () => {
|
|
it('should handle multiple consecutive close icon clicks', () => {
|
|
// Arrange
|
|
const onClose = jest.fn()
|
|
renderDrawer({ showClose: true, onClose })
|
|
|
|
// Act
|
|
const closeIcon = screen.getByTestId('close-icon')
|
|
fireEvent.click(closeIcon)
|
|
fireEvent.click(closeIcon)
|
|
fireEvent.click(closeIcon)
|
|
|
|
// Assert
|
|
expect(onClose).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('should handle onCancel and onOk being the same function', () => {
|
|
// Arrange
|
|
const handler = jest.fn()
|
|
renderDrawer({ onCancel: handler, onOk: handler })
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByText('common.operation.cancel'))
|
|
fireEvent.click(screen.getByText('common.operation.save'))
|
|
|
|
// Assert
|
|
expect(handler).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|
|
})
|