mirror of https://github.com/langgenius/dify.git
fix: update tests
This commit is contained in:
parent
2c609000ec
commit
ec70e7c82f
|
|
@ -11,27 +11,45 @@ import {
|
|||
DialogTrigger,
|
||||
} from '../index'
|
||||
|
||||
type PrimitiveProps = ComponentPropsWithoutRef<'div'>
|
||||
type DivPrimitiveProps = ComponentPropsWithoutRef<'div'>
|
||||
type ButtonPrimitiveProps = ComponentPropsWithoutRef<'button'>
|
||||
type SectionPrimitiveProps = ComponentPropsWithoutRef<'section'>
|
||||
|
||||
vi.mock('@base-ui/react/dialog', () => {
|
||||
const createPrimitive = (testId: string) => {
|
||||
return vi.fn(({ children, ...props }: PrimitiveProps) => (
|
||||
<div data-testid={testId} {...props}>
|
||||
const createDivPrimitive = () => {
|
||||
return vi.fn(({ children, ...props }: DivPrimitiveProps) => (
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
const createButtonPrimitive = () => {
|
||||
return vi.fn(({ children, ...props }: ButtonPrimitiveProps) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
|
||||
const createPopupPrimitive = () => {
|
||||
return vi.fn(({ children, ...props }: SectionPrimitiveProps) => (
|
||||
<section role="dialog" {...props}>
|
||||
{children}
|
||||
</section>
|
||||
))
|
||||
}
|
||||
|
||||
return {
|
||||
Dialog: {
|
||||
Root: createPrimitive('base-dialog-root'),
|
||||
Trigger: createPrimitive('base-dialog-trigger'),
|
||||
Title: createPrimitive('base-dialog-title'),
|
||||
Description: createPrimitive('base-dialog-description'),
|
||||
Close: createPrimitive('base-dialog-close'),
|
||||
Portal: createPrimitive('base-dialog-portal'),
|
||||
Backdrop: createPrimitive('base-dialog-backdrop'),
|
||||
Popup: createPrimitive('base-dialog-popup'),
|
||||
Root: createDivPrimitive(),
|
||||
Trigger: createDivPrimitive(),
|
||||
Title: createDivPrimitive(),
|
||||
Description: createDivPrimitive(),
|
||||
Close: createButtonPrimitive(),
|
||||
Portal: createDivPrimitive(),
|
||||
Backdrop: createDivPrimitive(),
|
||||
Popup: createPopupPrimitive(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -43,7 +61,7 @@ describe('Dialog wrapper', () => {
|
|||
|
||||
// Rendering behavior for wrapper-specific structure and content.
|
||||
describe('Rendering', () => {
|
||||
it('should render backdrop and popup when DialogContent is rendered', () => {
|
||||
it('should render portal structure and dialog content when DialogContent is rendered', () => {
|
||||
// Arrange
|
||||
const contentText = 'dialog body'
|
||||
|
||||
|
|
@ -55,81 +73,56 @@ describe('Dialog wrapper', () => {
|
|||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('base-dialog-portal')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('base-dialog-backdrop')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('base-dialog-popup')).toBeInTheDocument()
|
||||
expect(vi.mocked(BaseDialog.Portal)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(BaseDialog.Backdrop)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(BaseDialog.Popup)).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText(contentText)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply default wrapper class names when no override classes are provided', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<DialogContent>
|
||||
<span>content</span>
|
||||
</DialogContent>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const backdrop = screen.getByTestId('base-dialog-backdrop')
|
||||
const popup = screen.getByTestId('base-dialog-popup')
|
||||
|
||||
// Assert
|
||||
expect(backdrop).toHaveClass('fixed', 'inset-0', 'z-50', 'bg-background-overlay')
|
||||
expect(backdrop).toHaveClass('transition-opacity', 'duration-150')
|
||||
expect(backdrop).toHaveClass('data-[ending-style]:opacity-0', 'data-[starting-style]:opacity-0')
|
||||
|
||||
expect(popup).toHaveClass('fixed', 'left-1/2', 'top-1/2', 'z-50')
|
||||
expect(popup).toHaveClass('max-h-[80dvh]', 'w-[480px]', 'max-w-[calc(100vw-2rem)]')
|
||||
expect(popup).toHaveClass('-translate-x-1/2', '-translate-y-1/2')
|
||||
expect(popup).toHaveClass('rounded-2xl', 'border-[0.5px]', 'bg-components-panel-bg', 'p-6', 'shadow-xl')
|
||||
expect(popup).toHaveClass('transition-all', 'duration-150')
|
||||
expect(popup).toHaveClass(
|
||||
'data-[ending-style]:scale-95',
|
||||
'data-[starting-style]:scale-95',
|
||||
'data-[ending-style]:opacity-0',
|
||||
'data-[starting-style]:opacity-0',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Props behavior for class merging and custom styling.
|
||||
// Props behavior for closable semantics and defaults.
|
||||
describe('Props', () => {
|
||||
it('should merge overlayClassName and className with default classes when overrides are provided', () => {
|
||||
it('should not render close button when closable is omitted', () => {
|
||||
// Arrange
|
||||
const overlayClassName = 'custom-overlay opacity-90'
|
||||
const className = 'custom-popup max-w-[640px]'
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DialogContent overlayClassName={overlayClassName} className={className}>
|
||||
<DialogContent>
|
||||
<span>content</span>
|
||||
</DialogContent>,
|
||||
)
|
||||
|
||||
const backdrop = screen.getByTestId('base-dialog-backdrop')
|
||||
const popup = screen.getByTestId('base-dialog-popup')
|
||||
|
||||
// Assert
|
||||
expect(backdrop).toHaveClass('fixed', 'inset-0', 'custom-overlay', 'opacity-90')
|
||||
expect(popup).toHaveClass('fixed', 'left-1/2', 'custom-popup', 'max-w-[640px]')
|
||||
expect(popup).not.toHaveClass('max-w-[calc(100vw-2rem)]')
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render children inside popup when children are provided', () => {
|
||||
it('should not render close button when closable is false', () => {
|
||||
// Arrange
|
||||
const childText = 'child content'
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DialogContent>
|
||||
<div>{childText}</div>
|
||||
<DialogContent closable={false}>
|
||||
<span>content</span>
|
||||
</DialogContent>,
|
||||
)
|
||||
|
||||
const popup = screen.getByTestId('base-dialog-popup')
|
||||
// Assert
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render semantic close button when closable is true', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<DialogContent closable>
|
||||
<span>content</span>
|
||||
</DialogContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(popup).toContainElement(screen.getByText(childText))
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' })
|
||||
expect(closeButton).toHaveAttribute('aria-label', 'Close')
|
||||
expect(screen.getByRole('dialog')).toContainElement(closeButton)
|
||||
|
||||
const closeIcon = closeButton.querySelector('span')
|
||||
expect(closeIcon).toBeInTheDocument()
|
||||
expect(closeButton).toContainElement(closeIcon)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import type { Placement } from '@floating-ui/react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Menu } from '@base-ui/react/menu'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuCheckboxItemIndicator,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuGroupLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuRadioItemIndicator,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
|
|
@ -28,6 +23,11 @@ vi.mock('@base-ui/react/menu', async () => {
|
|||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
type PrimitiveOptions = {
|
||||
displayName: string
|
||||
defaultRole?: React.AriaRole
|
||||
}
|
||||
|
||||
type PositionerProps = PrimitiveProps & {
|
||||
side?: string
|
||||
align?: string
|
||||
|
|
@ -35,20 +35,33 @@ vi.mock('@base-ui/react/menu', async () => {
|
|||
alignOffset?: number
|
||||
}
|
||||
|
||||
const createPrimitive = (testId: string) => {
|
||||
const Primitive = React.forwardRef<HTMLDivElement, PrimitiveProps>(({ children, ...props }, ref) => {
|
||||
return React.createElement('div', { ref, 'data-testid': testId, ...props }, children)
|
||||
const createPrimitive = ({ displayName, defaultRole }: PrimitiveOptions) => {
|
||||
const Primitive = React.forwardRef<HTMLDivElement, PrimitiveProps>(({ children, role, ...props }, ref) => {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
ref,
|
||||
role: role ?? defaultRole,
|
||||
...props,
|
||||
},
|
||||
children,
|
||||
)
|
||||
})
|
||||
Primitive.displayName = testId
|
||||
Primitive.displayName = displayName
|
||||
return Primitive
|
||||
}
|
||||
|
||||
const Positioner = React.forwardRef<HTMLDivElement, PositionerProps>(({ children, side, align, sideOffset, alignOffset, ...props }, ref) => {
|
||||
const Portal = ({ children }: PrimitiveProps) => {
|
||||
return React.createElement(React.Fragment, null, children)
|
||||
}
|
||||
Portal.displayName = 'menu-portal'
|
||||
|
||||
const Positioner = React.forwardRef<HTMLDivElement, PositionerProps>(({ children, role, side, align, sideOffset, alignOffset, ...props }, ref) => {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
ref,
|
||||
'data-testid': 'menu-positioner',
|
||||
'role': role ?? 'group',
|
||||
'data-side': side,
|
||||
'data-align': align,
|
||||
'data-side-offset': sideOffset,
|
||||
|
|
@ -61,22 +74,22 @@ vi.mock('@base-ui/react/menu', async () => {
|
|||
Positioner.displayName = 'menu-positioner'
|
||||
|
||||
const Menu = {
|
||||
Root: createPrimitive('menu-root'),
|
||||
Portal: createPrimitive('menu-portal'),
|
||||
Trigger: createPrimitive('menu-trigger'),
|
||||
SubmenuRoot: createPrimitive('menu-submenu-root'),
|
||||
Group: createPrimitive('menu-group'),
|
||||
GroupLabel: createPrimitive('menu-group-label'),
|
||||
RadioGroup: createPrimitive('menu-radio-group'),
|
||||
RadioItem: createPrimitive('menu-radio-item'),
|
||||
RadioItemIndicator: createPrimitive('menu-radio-item-indicator'),
|
||||
CheckboxItem: createPrimitive('menu-checkbox-item'),
|
||||
CheckboxItemIndicator: createPrimitive('menu-checkbox-item-indicator'),
|
||||
Root: createPrimitive({ displayName: 'menu-root' }),
|
||||
Portal,
|
||||
Trigger: createPrimitive({ displayName: 'menu-trigger', defaultRole: 'button' }),
|
||||
SubmenuRoot: createPrimitive({ displayName: 'menu-submenu-root' }),
|
||||
Group: createPrimitive({ displayName: 'menu-group', defaultRole: 'group' }),
|
||||
GroupLabel: createPrimitive({ displayName: 'menu-group-label' }),
|
||||
RadioGroup: createPrimitive({ displayName: 'menu-radio-group', defaultRole: 'radiogroup' }),
|
||||
RadioItem: createPrimitive({ displayName: 'menu-radio-item', defaultRole: 'menuitemradio' }),
|
||||
RadioItemIndicator: createPrimitive({ displayName: 'menu-radio-item-indicator' }),
|
||||
CheckboxItem: createPrimitive({ displayName: 'menu-checkbox-item', defaultRole: 'menuitemcheckbox' }),
|
||||
CheckboxItemIndicator: createPrimitive({ displayName: 'menu-checkbox-item-indicator' }),
|
||||
Positioner,
|
||||
Popup: createPrimitive('menu-popup'),
|
||||
SubmenuTrigger: createPrimitive('menu-submenu-trigger'),
|
||||
Item: createPrimitive('menu-item'),
|
||||
Separator: createPrimitive('menu-separator'),
|
||||
Popup: createPrimitive({ displayName: 'menu-popup', defaultRole: 'menu' }),
|
||||
SubmenuTrigger: createPrimitive({ displayName: 'menu-submenu-trigger', defaultRole: 'menuitem' }),
|
||||
Item: createPrimitive({ displayName: 'menu-item', defaultRole: 'menuitem' }),
|
||||
Separator: createPrimitive({ displayName: 'menu-separator', defaultRole: 'separator' }),
|
||||
}
|
||||
|
||||
return { Menu }
|
||||
|
|
@ -99,7 +112,7 @@ describe('dropdown-menu wrapper', () => {
|
|||
|
||||
// Ensures exported aliases stay aligned with the wrapped Menu primitives.
|
||||
describe('alias exports', () => {
|
||||
it('should map each alias export to the corresponding Menu primitive', () => {
|
||||
it('should map direct aliases to the corresponding Menu primitive when importing menu roots', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
|
|
@ -110,44 +123,38 @@ describe('dropdown-menu wrapper', () => {
|
|||
expect(DropdownMenuTrigger).toBe(Menu.Trigger)
|
||||
expect(DropdownMenuSub).toBe(Menu.SubmenuRoot)
|
||||
expect(DropdownMenuGroup).toBe(Menu.Group)
|
||||
expect(DropdownMenuGroupLabel).toBe(Menu.GroupLabel)
|
||||
expect(DropdownMenuRadioGroup).toBe(Menu.RadioGroup)
|
||||
expect(DropdownMenuRadioItem).toBe(Menu.RadioItem)
|
||||
expect(DropdownMenuRadioItemIndicator).toBe(Menu.RadioItemIndicator)
|
||||
expect(DropdownMenuCheckboxItem).toBe(Menu.CheckboxItem)
|
||||
expect(DropdownMenuCheckboxItemIndicator).toBe(Menu.CheckboxItemIndicator)
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies content popup placement and passthrough behavior.
|
||||
describe('DropdownMenuContent', () => {
|
||||
it('should use default placement and offsets when props are omitted', () => {
|
||||
it('should position content at bottom-end with default offsets when placement props are omitted', () => {
|
||||
// Arrange
|
||||
const parsePlacementMock = vi.mocked(parsePlacement)
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuContent>
|
||||
<span>content child</span>
|
||||
<button type="button">content action</button>
|
||||
</DropdownMenuContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByTestId('menu-popup')
|
||||
const popup = screen.getByRole('menu')
|
||||
const positioner = popup.parentElement
|
||||
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('bottom-end')
|
||||
expect(positioner).not.toBeNull()
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '4')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '0')
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(popup).toHaveClass('rounded-xl')
|
||||
expect(popup).toHaveClass('py-1')
|
||||
expect(screen.getByText('content child')).toBeInTheDocument()
|
||||
expect(within(popup).getByRole('button', { name: 'content action' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should parse custom placement and merge custom class names', () => {
|
||||
it('should apply custom placement offsets when custom positioning props are provided', () => {
|
||||
// Arrange
|
||||
const parsePlacementMock = vi.mocked(parsePlacement)
|
||||
|
||||
|
|
@ -157,41 +164,42 @@ describe('dropdown-menu wrapper', () => {
|
|||
placement="top-start"
|
||||
sideOffset={12}
|
||||
alignOffset={-3}
|
||||
className="content-positioner-custom"
|
||||
popupClassName="content-popup-custom"
|
||||
>
|
||||
<span>custom content</span>
|
||||
</DropdownMenuContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByTestId('menu-popup')
|
||||
const popup = screen.getByRole('menu')
|
||||
const positioner = popup.parentElement
|
||||
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('top-start')
|
||||
expect(positioner).not.toBeNull()
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '12')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '-3')
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(positioner).toHaveClass('content-positioner-custom')
|
||||
expect(popup).toHaveClass('content-popup-custom')
|
||||
expect(screen.getByText('custom content')).toBeInTheDocument()
|
||||
expect(within(popup).getByText('custom content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward positioner and popup passthrough props when passthrough props are provided', () => {
|
||||
it('should forward passthrough attributes and handlers when positionerProps and popupProps are provided', () => {
|
||||
// Arrange
|
||||
const handlePositionerMouseEnter = vi.fn()
|
||||
const handlePopupClick = vi.fn()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuContent
|
||||
positionerProps={{
|
||||
'aria-label': 'dropdown content positioner',
|
||||
'id': 'dropdown-content-positioner',
|
||||
'onMouseEnter': handlePositionerMouseEnter,
|
||||
}}
|
||||
popupProps={{
|
||||
'role': 'menu',
|
||||
'aria-label': 'dropdown content popup',
|
||||
'id': 'dropdown-content-popup',
|
||||
'onClick': handlePopupClick,
|
||||
}}
|
||||
>
|
||||
<span>passthrough content</span>
|
||||
|
|
@ -199,41 +207,50 @@ describe('dropdown-menu wrapper', () => {
|
|||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByTestId('menu-popup')
|
||||
expect(positioner).toHaveAttribute('aria-label', 'dropdown content positioner')
|
||||
expect(popup).toHaveAttribute('role', 'menu')
|
||||
expect(popup).toHaveAttribute('aria-label', 'dropdown content popup')
|
||||
const positioner = screen.getByRole('group', { name: 'dropdown content positioner' })
|
||||
const popup = screen.getByRole('menu', { name: 'dropdown content popup' })
|
||||
fireEvent.mouseEnter(positioner)
|
||||
fireEvent.click(popup)
|
||||
|
||||
expect(positioner).toHaveAttribute('id', 'dropdown-content-positioner')
|
||||
expect(popup).toHaveAttribute('id', 'dropdown-content-popup')
|
||||
expect(handlePositionerMouseEnter).toHaveBeenCalledTimes(1)
|
||||
expect(handlePopupClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies submenu popup placement and passthrough behavior.
|
||||
describe('DropdownMenuSubContent', () => {
|
||||
it('should use the default sub-content placement and offsets', () => {
|
||||
it('should position sub-content at left-start with default offsets when props are omitted', () => {
|
||||
// Arrange
|
||||
const parsePlacementMock = vi.mocked(parsePlacement)
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuSubContent>
|
||||
<span>sub content child</span>
|
||||
<button type="button">sub action</button>
|
||||
</DropdownMenuSubContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByRole('menu')
|
||||
const positioner = popup.parentElement
|
||||
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('left-start')
|
||||
expect(positioner).not.toBeNull()
|
||||
expect(positioner).toHaveAttribute('data-side', 'left')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '4')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '0')
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(screen.getByText('sub content child')).toBeInTheDocument()
|
||||
expect(within(popup).getByRole('button', { name: 'sub action' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should parse custom placement and merge popup class names', () => {
|
||||
it('should apply custom placement offsets and forward passthrough props when custom sub-content props are provided', () => {
|
||||
// Arrange
|
||||
const parsePlacementMock = vi.mocked(parsePlacement)
|
||||
const handlePositionerFocus = vi.fn()
|
||||
const handlePopupClick = vi.fn()
|
||||
|
||||
// Act
|
||||
render(
|
||||
|
|
@ -241,16 +258,26 @@ describe('dropdown-menu wrapper', () => {
|
|||
placement="right-end"
|
||||
sideOffset={6}
|
||||
alignOffset={2}
|
||||
className="sub-positioner-custom"
|
||||
popupClassName="sub-popup-custom"
|
||||
positionerProps={{
|
||||
'aria-label': 'dropdown sub positioner',
|
||||
'id': 'dropdown-sub-positioner',
|
||||
'onFocus': handlePositionerFocus,
|
||||
}}
|
||||
popupProps={{
|
||||
'aria-label': 'dropdown sub popup',
|
||||
'id': 'dropdown-sub-popup',
|
||||
'onClick': handlePopupClick,
|
||||
}}
|
||||
>
|
||||
<span>custom sub content</span>
|
||||
</DropdownMenuSubContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByTestId('menu-popup')
|
||||
const positioner = screen.getByRole('group', { name: 'dropdown sub positioner' })
|
||||
const popup = screen.getByRole('menu', { name: 'dropdown sub popup' })
|
||||
fireEvent.focus(positioner)
|
||||
fireEvent.click(popup)
|
||||
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('right-end')
|
||||
|
|
@ -258,117 +285,123 @@ describe('dropdown-menu wrapper', () => {
|
|||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '6')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '2')
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(positioner).toHaveClass('sub-positioner-custom')
|
||||
expect(popup).toHaveClass('sub-popup-custom')
|
||||
})
|
||||
|
||||
it('should forward passthrough props for sub-content positioner and popup when passthrough props are provided', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuSubContent
|
||||
positionerProps={{
|
||||
'aria-label': 'dropdown sub positioner',
|
||||
}}
|
||||
popupProps={{
|
||||
'role': 'menu',
|
||||
'aria-label': 'dropdown sub popup',
|
||||
}}
|
||||
>
|
||||
<span>passthrough sub content</span>
|
||||
</DropdownMenuSubContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('menu-positioner')
|
||||
const popup = screen.getByTestId('menu-popup')
|
||||
expect(positioner).toHaveAttribute('aria-label', 'dropdown sub positioner')
|
||||
expect(popup).toHaveAttribute('role', 'menu')
|
||||
expect(popup).toHaveAttribute('aria-label', 'dropdown sub popup')
|
||||
expect(positioner).toHaveAttribute('id', 'dropdown-sub-positioner')
|
||||
expect(popup).toHaveAttribute('id', 'dropdown-sub-popup')
|
||||
expect(handlePositionerFocus).toHaveBeenCalledTimes(1)
|
||||
expect(handlePopupClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Covers submenu trigger behavior with and without destructive flag.
|
||||
describe('DropdownMenuSubTrigger', () => {
|
||||
it('should merge className and apply destructive style when destructive is true', () => {
|
||||
it('should render label and submenu chevron when trigger children are provided', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuSubTrigger className="sub-trigger-custom" destructive>
|
||||
<DropdownMenuSubTrigger>
|
||||
Trigger item
|
||||
</DropdownMenuSubTrigger>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const subTrigger = screen.getByTestId('menu-submenu-trigger')
|
||||
expect(subTrigger).toHaveClass('mx-1')
|
||||
expect(subTrigger).toHaveClass('sub-trigger-custom')
|
||||
expect(subTrigger).toHaveClass('text-text-destructive')
|
||||
const subTrigger = screen.getByRole('menuitem', { name: 'Trigger item' })
|
||||
expect(subTrigger.querySelector('span[aria-hidden="true"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not apply destructive style when destructive is false', () => {
|
||||
it.each([true, false])('should remain interactive and not leak destructive prop when destructive is %s', (destructive) => {
|
||||
// Arrange
|
||||
const handleClick = vi.fn()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuSubTrigger className="sub-trigger-custom">
|
||||
<DropdownMenuSubTrigger
|
||||
destructive={destructive}
|
||||
aria-label="submenu action"
|
||||
id={`submenu-trigger-${String(destructive)}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Trigger item
|
||||
</DropdownMenuSubTrigger>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('menu-submenu-trigger')).not.toHaveClass('text-text-destructive')
|
||||
const subTrigger = screen.getByRole('menuitem', { name: 'submenu action' })
|
||||
fireEvent.click(subTrigger)
|
||||
|
||||
expect(subTrigger).toHaveAttribute('id', `submenu-trigger-${String(destructive)}`)
|
||||
expect(subTrigger).not.toHaveAttribute('destructive')
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Covers menu item behavior with and without destructive flag.
|
||||
describe('DropdownMenuItem', () => {
|
||||
it('should merge className and apply destructive style when destructive is true', () => {
|
||||
it.each([true, false])('should remain interactive and not leak destructive prop when destructive is %s', (destructive) => {
|
||||
// Arrange
|
||||
const handleClick = vi.fn()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuItem className="item-custom" destructive>
|
||||
<DropdownMenuItem
|
||||
destructive={destructive}
|
||||
aria-label="menu action"
|
||||
id={`menu-item-${String(destructive)}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Item label
|
||||
</DropdownMenuItem>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const item = screen.getByTestId('menu-item')
|
||||
expect(item).toHaveClass('mx-1')
|
||||
expect(item).toHaveClass('item-custom')
|
||||
expect(item).toHaveClass('text-text-destructive')
|
||||
})
|
||||
const item = screen.getByRole('menuitem', { name: 'menu action' })
|
||||
fireEvent.click(item)
|
||||
|
||||
it('should not apply destructive style when destructive is false', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuItem className="item-custom">
|
||||
Item label
|
||||
</DropdownMenuItem>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('menu-item')).not.toHaveClass('text-text-destructive')
|
||||
expect(item).toHaveAttribute('id', `menu-item-${String(destructive)}`)
|
||||
expect(item).not.toHaveAttribute('destructive')
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies separator semantics and row separation behavior.
|
||||
describe('DropdownMenuSeparator', () => {
|
||||
it('should merge custom class names with default separator classes', () => {
|
||||
it('should forward passthrough props and handlers when separator props are provided', () => {
|
||||
// Arrange
|
||||
const handleMouseEnter = vi.fn()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DropdownMenuSeparator
|
||||
aria-label="actions divider"
|
||||
id="menu-separator"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const separator = screen.getByRole('separator', { name: 'actions divider' })
|
||||
fireEvent.mouseEnter(separator)
|
||||
|
||||
expect(separator).toHaveAttribute('id', 'menu-separator')
|
||||
expect(handleMouseEnter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should keep surrounding menu rows rendered when separator is placed between items', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
render(<DropdownMenuSeparator className="separator-custom" />)
|
||||
render(
|
||||
<>
|
||||
<DropdownMenuItem>First action</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Second action</DropdownMenuItem>
|
||||
</>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const separator = screen.getByTestId('menu-separator')
|
||||
expect(separator).toHaveClass('my-1')
|
||||
expect(separator).toHaveClass('h-px')
|
||||
expect(separator).toHaveClass('bg-divider-regular')
|
||||
expect(separator).toHaveClass('separator-custom')
|
||||
expect(screen.getByRole('menuitem', { name: 'First action' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'Second action' })).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('separator')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,68 +1,85 @@
|
|||
import type { Placement } from '@floating-ui/react'
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
||||
import { Popover as BasePopover } from '@base-ui/react/popover'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { PopoverContent } from '..'
|
||||
import {
|
||||
Popover,
|
||||
PopoverClose,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from '..'
|
||||
|
||||
type ParsedPlacement = {
|
||||
side: 'top' | 'bottom' | 'left' | 'right'
|
||||
align: 'start' | 'center' | 'end'
|
||||
type PrimitiveProps = ComponentPropsWithoutRef<'div'> & {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
type PositionerMockProps = ComponentPropsWithoutRef<'div'> & {
|
||||
side?: ParsedPlacement['side']
|
||||
align?: ParsedPlacement['align']
|
||||
type PositionerProps = PrimitiveProps & {
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
}
|
||||
|
||||
const positionerPropsSpy = vi.fn<(props: PositionerMockProps) => void>()
|
||||
const popupClassNameSpy = vi.fn<(className: string | undefined) => void>()
|
||||
const popupPropsSpy = vi.fn<(props: ComponentPropsWithoutRef<'div'>) => void>()
|
||||
const parsePlacementMock = vi.fn<(placement: Placement) => ParsedPlacement>()
|
||||
|
||||
vi.mock('@base-ui/react/popover', () => {
|
||||
const Root = ({ children, ...props }: ComponentPropsWithoutRef<'div'>) => (
|
||||
<div {...props}>{children}</div>
|
||||
const Root = ({ children, ...props }: PrimitiveProps) => (
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Trigger = ({ children, ...props }: ComponentPropsWithoutRef<'button'>) => (
|
||||
<button type="button" {...props}>{children}</button>
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
const Close = ({ children, ...props }: ComponentPropsWithoutRef<'button'>) => (
|
||||
<button type="button" {...props}>{children}</button>
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
const Title = ({ children, ...props }: ComponentPropsWithoutRef<'h2'>) => (
|
||||
<h2 {...props}>{children}</h2>
|
||||
<h2 {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
|
||||
const Description = ({ children, ...props }: ComponentPropsWithoutRef<'p'>) => (
|
||||
<p {...props}>{children}</p>
|
||||
<p {...props}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
|
||||
const Portal = ({ children }: { children?: ReactNode }) => (
|
||||
<div data-testid="mock-portal">{children}</div>
|
||||
const Portal = ({ children }: PrimitiveProps) => (
|
||||
<div>{children}</div>
|
||||
)
|
||||
|
||||
const Positioner = ({ children, ...props }: PositionerMockProps) => {
|
||||
positionerPropsSpy(props)
|
||||
return (
|
||||
<div data-testid="mock-positioner" className={props.className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Positioner = ({
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
sideOffset,
|
||||
alignOffset,
|
||||
...props
|
||||
}: PositionerProps) => (
|
||||
<div
|
||||
data-side={side}
|
||||
data-align={align}
|
||||
data-side-offset={String(sideOffset)}
|
||||
data-align-offset={String(alignOffset)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Popup = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
popupClassNameSpy(className)
|
||||
popupPropsSpy({ className, ...props })
|
||||
return (
|
||||
<div data-testid="mock-popup" className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Popup = ({ children, ...props }: PrimitiveProps) => (
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
return {
|
||||
Popover: {
|
||||
|
|
@ -78,98 +95,57 @@ vi.mock('@base-ui/react/popover', () => {
|
|||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/ui/placement', () => ({
|
||||
parsePlacement: (placement: Placement) => parsePlacementMock(placement),
|
||||
}))
|
||||
|
||||
describe('PopoverContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
parsePlacementMock.mockReturnValue({
|
||||
side: 'bottom',
|
||||
align: 'center',
|
||||
})
|
||||
})
|
||||
|
||||
describe('Default props', () => {
|
||||
it('should use bottom placement and default offsets when optional props are not provided', () => {
|
||||
// Placement and default value behaviors.
|
||||
describe('Placement', () => {
|
||||
it('should use bottom placement and default offsets when placement props are not provided', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<PopoverContent>
|
||||
<PopoverContent positionerProps={{ 'aria-label': 'default positioner' }}>
|
||||
<span>Default content</span>
|
||||
</PopoverContent>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const positioner = screen.getByTestId('mock-positioner')
|
||||
const positioner = screen.getByLabelText('default positioner')
|
||||
|
||||
// Assert
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('bottom')
|
||||
expect(positionerPropsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
side: 'bottom',
|
||||
align: 'center',
|
||||
sideOffset: 8,
|
||||
alignOffset: 0,
|
||||
}),
|
||||
)
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'center')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '8')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '0')
|
||||
expect(screen.getByText('Default content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Placement parsing', () => {
|
||||
it('should use parsePlacement output and forward custom placement offsets to Positioner', () => {
|
||||
// Arrange
|
||||
parsePlacementMock.mockReturnValue({
|
||||
side: 'left',
|
||||
align: 'end',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(
|
||||
<PopoverContent placement="top-end" sideOffset={14} alignOffset={6}>
|
||||
<span>Parsed content</span>
|
||||
</PopoverContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(parsePlacementMock).toHaveBeenCalledTimes(1)
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('top-end')
|
||||
expect(positionerPropsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
side: 'left',
|
||||
align: 'end',
|
||||
sideOffset: 14,
|
||||
alignOffset: 6,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ClassName behavior', () => {
|
||||
it('should merge custom className values into Positioner and Popup class names', () => {
|
||||
it('should apply parsed custom placement and custom offsets when placement props are provided', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<PopoverContent className="custom-positioner" popupClassName="custom-popup">
|
||||
<span>Styled content</span>
|
||||
<PopoverContent
|
||||
placement="top-end"
|
||||
sideOffset={14}
|
||||
alignOffset={6}
|
||||
positionerProps={{ 'aria-label': 'custom positioner' }}
|
||||
>
|
||||
<span>Custom placement content</span>
|
||||
</PopoverContent>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const positioner = screen.getByTestId('mock-positioner')
|
||||
const popup = screen.getByTestId('mock-popup')
|
||||
const positioner = screen.getByLabelText('custom positioner')
|
||||
|
||||
// Assert
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(positioner).toHaveClass('custom-positioner')
|
||||
expect(popup).toHaveClass('rounded-xl')
|
||||
expect(popup).toHaveClass('custom-popup')
|
||||
expect(popupClassNameSpy).toHaveBeenCalledWith(expect.stringContaining('custom-popup'))
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '14')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '6')
|
||||
})
|
||||
})
|
||||
|
||||
// Passthrough behavior for delegated primitives.
|
||||
describe('Passthrough props', () => {
|
||||
it('should forward positionerProps and popupProps when passthrough props are provided', () => {
|
||||
// Arrange
|
||||
|
|
@ -179,6 +155,7 @@ describe('PopoverContent', () => {
|
|||
'aria-label': 'popover positioner',
|
||||
}}
|
||||
popupProps={{
|
||||
'id': 'popover-popup-id',
|
||||
'role': 'dialog',
|
||||
'aria-label': 'popover content',
|
||||
}}
|
||||
|
|
@ -188,39 +165,35 @@ describe('PopoverContent', () => {
|
|||
)
|
||||
|
||||
// Act
|
||||
const popup = screen.getByTestId('mock-popup')
|
||||
const positioner = screen.getByLabelText('popover positioner')
|
||||
const popup = screen.getByRole('dialog', { name: 'popover content' })
|
||||
|
||||
// Assert
|
||||
expect(positionerPropsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
'aria-label': 'popover positioner',
|
||||
}),
|
||||
)
|
||||
expect(popupPropsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
'role': 'dialog',
|
||||
'aria-label': 'popover content',
|
||||
}),
|
||||
)
|
||||
expect(positioner).toHaveAttribute('aria-label', 'popover positioner')
|
||||
expect(popup).toHaveAttribute('id', 'popover-popup-id')
|
||||
expect(popup).toHaveAttribute('role', 'dialog')
|
||||
expect(popup).toHaveAttribute('aria-label', 'popover content')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Children rendering', () => {
|
||||
it('should render children inside Popup', () => {
|
||||
describe('Popover aliases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Export mapping behavior to keep wrapper aliases aligned.
|
||||
describe('Export mapping', () => {
|
||||
it('should map aliases to the matching base popover primitives when wrapper exports are imported', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<PopoverContent>
|
||||
<button type="button">Child action</button>
|
||||
</PopoverContent>,
|
||||
)
|
||||
const basePrimitives = BasePopover
|
||||
|
||||
// Act
|
||||
const popup = screen.getByTestId('mock-popup')
|
||||
|
||||
// Assert
|
||||
expect(popup).toContainElement(screen.getByRole('button', { name: 'Child action' }))
|
||||
// Act & Assert
|
||||
expect(Popover).toBe(basePrimitives.Root)
|
||||
expect(PopoverTrigger).toBe(basePrimitives.Trigger)
|
||||
expect(PopoverClose).toBe(basePrimitives.Close)
|
||||
expect(PopoverTitle).toBe(basePrimitives.Title)
|
||||
expect(PopoverDescription).toBe(basePrimitives.Description)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import type { Placement } from '@floating-ui/react'
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
HTMLAttributes,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
SelectContent,
|
||||
|
|
@ -18,7 +17,7 @@ type ParsedPlacement = {
|
|||
}
|
||||
|
||||
const { mockParsePlacement } = vi.hoisted(() => ({
|
||||
mockParsePlacement: vi.fn<(placement: Placement) => ParsedPlacement>(),
|
||||
mockParsePlacement: vi.fn<(placement: string) => ParsedPlacement>(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/placement', () => ({
|
||||
|
|
@ -32,41 +31,43 @@ vi.mock('@base-ui/react/select', () => {
|
|||
align?: 'start' | 'center' | 'end'
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
alignItemWithTrigger?: boolean
|
||||
}
|
||||
|
||||
const Root = ({ children }: WithChildren) => <div data-testid="base-select-root">{children}</div>
|
||||
const Value = ({ children }: WithChildren) => <span data-testid="base-select-value">{children}</span>
|
||||
const Group = ({ children }: WithChildren) => <div data-testid="base-select-group">{children}</div>
|
||||
const GroupLabel = ({ children }: WithChildren) => <div data-testid="base-select-group-label">{children}</div>
|
||||
const Separator = (props: HTMLAttributes<HTMLHRElement>) => <hr data-testid="base-select-separator" {...props} />
|
||||
const Root = ({ children }: WithChildren) => <div>{children}</div>
|
||||
const Value = ({ children }: WithChildren) => <span>{children}</span>
|
||||
const Group = ({ children }: WithChildren) => <div>{children}</div>
|
||||
const GroupLabel = ({ children }: WithChildren) => <div>{children}</div>
|
||||
const Separator = (props: HTMLAttributes<HTMLHRElement>) => <hr {...props} />
|
||||
|
||||
const Trigger = ({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button data-testid="base-select-trigger" type="button" {...props}>
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
const Icon = ({ children, ...props }: HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span data-testid="base-select-icon" {...props}>
|
||||
<span aria-label="Open select menu" role="img" {...props}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
const Portal = ({ children }: WithChildren) => <div data-testid="base-select-portal">{children}</div>
|
||||
const Portal = ({ children }: WithChildren) => <div>{children}</div>
|
||||
const Positioner = ({
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
sideOffset,
|
||||
alignOffset,
|
||||
alignItemWithTrigger,
|
||||
className,
|
||||
...props
|
||||
}: PositionerProps) => (
|
||||
<div
|
||||
data-align={align}
|
||||
data-align-offset={alignOffset}
|
||||
data-align-item-with-trigger={alignItemWithTrigger === undefined ? undefined : String(alignItemWithTrigger)}
|
||||
data-side={side}
|
||||
data-side-offset={sideOffset}
|
||||
data-testid="base-select-positioner"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -74,28 +75,28 @@ vi.mock('@base-ui/react/select', () => {
|
|||
</div>
|
||||
)
|
||||
const Popup = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div data-testid="base-select-popup" {...props}>
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
const List = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div data-testid="base-select-list" {...props}>
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Item = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div data-testid="base-select-item" {...props}>
|
||||
<div role="option" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
const ItemText = ({ children, ...props }: HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span data-testid="base-select-item-text" {...props}>
|
||||
<span {...props}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
const ItemIndicator = ({ children, ...props }: HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span data-testid="base-select-item-indicator" {...props}>
|
||||
<span aria-label="Selected item indicator" role="img" {...props}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
|
@ -126,14 +127,42 @@ describe('Select wrappers', () => {
|
|||
mockParsePlacement.mockReturnValue({ side: 'bottom', align: 'start' })
|
||||
})
|
||||
|
||||
// Covers trigger-level wrapper behavior.
|
||||
// Covers default rendering and visual branches for trigger content.
|
||||
describe('SelectTrigger', () => {
|
||||
it('should forward trigger props when trigger props are provided', () => {
|
||||
it('should render the default icon when clearable and loading are not enabled', () => {
|
||||
// Arrange
|
||||
render(<SelectTrigger>Trigger Label</SelectTrigger>)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Trigger Label')).toBeInTheDocument()
|
||||
expect(screen.getByRole('img', { name: /open select menu/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /clear selection/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render clear button when clearable is true and loading is false', () => {
|
||||
// Arrange
|
||||
render(<SelectTrigger clearable>Trigger Label</SelectTrigger>)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /clear selection/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('img', { name: /open select menu/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading indicator and hide clear button when loading is true', () => {
|
||||
// Arrange
|
||||
render(<SelectTrigger clearable loading>Trigger Label</SelectTrigger>)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /trigger label/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /clear selection/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('img', { name: /open select menu/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward native trigger props when trigger props are provided', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<SelectTrigger
|
||||
aria-label="Choose option"
|
||||
data-testid="custom-trigger"
|
||||
disabled
|
||||
>
|
||||
Trigger Label
|
||||
|
|
@ -141,44 +170,71 @@ describe('Select wrappers', () => {
|
|||
)
|
||||
|
||||
// Assert
|
||||
const trigger = screen.getByTestId('custom-trigger')
|
||||
const trigger = screen.getByRole('button', { name: /choose option/i })
|
||||
expect(trigger).toBeDisabled()
|
||||
expect(trigger).toHaveAttribute('aria-label', 'Choose option')
|
||||
expect(trigger).toHaveAttribute('data-testid', 'custom-trigger')
|
||||
})
|
||||
|
||||
it('should compose default and custom class names when className is provided', () => {
|
||||
// Arrange & Act
|
||||
render(<SelectTrigger className="custom-trigger-class">Trigger Label</SelectTrigger>)
|
||||
it('should call onClear and stop click propagation when clear button is clicked', () => {
|
||||
// Arrange
|
||||
const onClear = vi.fn()
|
||||
const onTriggerClick = vi.fn()
|
||||
render(
|
||||
<SelectTrigger clearable onClear={onClear} onClick={onTriggerClick}>
|
||||
Trigger Label
|
||||
</SelectTrigger>,
|
||||
)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /clear selection/i }))
|
||||
|
||||
// Assert
|
||||
const trigger = screen.getByTestId('base-select-trigger')
|
||||
expect(trigger).toHaveClass('group')
|
||||
expect(trigger).toHaveClass('h-8')
|
||||
expect(trigger).toHaveClass('custom-trigger-class')
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
expect(onTriggerClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render children and icon when content is provided', () => {
|
||||
// Arrange & Act
|
||||
render(<SelectTrigger>Trigger Label</SelectTrigger>)
|
||||
it('should stop mouse down propagation when clear button receives mouse down', () => {
|
||||
// Arrange
|
||||
const onTriggerMouseDown = vi.fn()
|
||||
render(
|
||||
<SelectTrigger clearable onMouseDown={onTriggerMouseDown}>
|
||||
Trigger Label
|
||||
</SelectTrigger>,
|
||||
)
|
||||
|
||||
// Act
|
||||
fireEvent.mouseDown(screen.getByRole('button', { name: /clear selection/i }))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Trigger Label')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('base-select-icon')).toBeInTheDocument()
|
||||
expect(onTriggerMouseDown).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not throw when clear button is clicked without onClear handler', () => {
|
||||
// Arrange
|
||||
render(<SelectTrigger clearable>Trigger Label</SelectTrigger>)
|
||||
const clearButton = screen.getByRole('button', { name: /clear selection/i })
|
||||
|
||||
// Act & Assert
|
||||
expect(() => fireEvent.click(clearButton)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// Covers content placement parsing and positioner forwarding.
|
||||
// Covers content placement parsing, forwarding props, and slot rendering.
|
||||
describe('SelectContent', () => {
|
||||
it('should call parsePlacement with default placement when placement is not provided', () => {
|
||||
// Arrange
|
||||
mockParsePlacement.mockReturnValueOnce({ side: 'bottom', align: 'start' })
|
||||
|
||||
// Act
|
||||
render(<SelectContent><div>Option A</div></SelectContent>)
|
||||
render(
|
||||
<SelectContent>
|
||||
<span>Option A</span>
|
||||
</SelectContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockParsePlacement).toHaveBeenCalledWith('bottom-start')
|
||||
expect(screen.getByText('Option A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass parsed side align and offsets to Positioner when custom placement and offsets are provided', () => {
|
||||
|
|
@ -186,37 +242,25 @@ describe('Select wrappers', () => {
|
|||
mockParsePlacement.mockReturnValueOnce({ side: 'top', align: 'end' })
|
||||
|
||||
// Act
|
||||
render(
|
||||
<SelectContent alignOffset={6} placement="top-end" sideOffset={12}>
|
||||
<div>Option A</div>
|
||||
</SelectContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('base-select-positioner')
|
||||
expect(mockParsePlacement).toHaveBeenCalledWith('top-end')
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '12')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '6')
|
||||
})
|
||||
|
||||
it('should compose positioner popup and list class names when custom class props are provided', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<SelectContent
|
||||
className="custom-positioner"
|
||||
listClassName="custom-list"
|
||||
popupClassName="custom-popup"
|
||||
alignOffset={6}
|
||||
placement="top-end"
|
||||
sideOffset={12}
|
||||
positionerProps={{ 'role': 'group', 'aria-label': 'Select positioner' }}
|
||||
>
|
||||
<div>Option A</div>
|
||||
</SelectContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('base-select-positioner')).toHaveClass('outline-none', 'custom-positioner')
|
||||
expect(screen.getByTestId('base-select-popup')).toHaveClass('rounded-xl', 'custom-popup')
|
||||
expect(screen.getByTestId('base-select-list')).toHaveClass('max-h-80', 'custom-list')
|
||||
const positioner = screen.getByRole('group', { name: /select positioner/i })
|
||||
expect(mockParsePlacement).toHaveBeenCalledWith('top-end')
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '12')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '6')
|
||||
expect(positioner).toHaveAttribute('data-align-item-with-trigger', 'false')
|
||||
})
|
||||
|
||||
it('should forward passthrough props to positioner popup and list when passthrough props are provided', () => {
|
||||
|
|
@ -224,6 +268,7 @@ describe('Select wrappers', () => {
|
|||
render(
|
||||
<SelectContent
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'select positioner',
|
||||
}}
|
||||
popupProps={{
|
||||
|
|
@ -240,9 +285,9 @@ describe('Select wrappers', () => {
|
|||
)
|
||||
|
||||
// Assert
|
||||
const positioner = screen.getByTestId('base-select-positioner')
|
||||
const popup = screen.getByTestId('base-select-popup')
|
||||
const list = screen.getByTestId('base-select-list')
|
||||
const positioner = screen.getByRole('group', { name: /select positioner/i })
|
||||
const popup = screen.getByRole('dialog', { name: /select popup/i })
|
||||
const list = screen.getByRole('listbox', { name: /select list/i })
|
||||
|
||||
expect(positioner).toHaveAttribute('aria-label', 'select positioner')
|
||||
expect(popup).toHaveAttribute('role', 'dialog')
|
||||
|
|
@ -250,46 +295,31 @@ describe('Select wrappers', () => {
|
|||
expect(list).toHaveAttribute('role', 'listbox')
|
||||
expect(list).toHaveAttribute('aria-label', 'select list')
|
||||
})
|
||||
|
||||
it('should render children inside list when children are provided', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<SelectContent>
|
||||
<span data-testid="list-child">Option A</span>
|
||||
</SelectContent>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const list = screen.getByTestId('base-select-list')
|
||||
expect(list).toContainElement(screen.getByTestId('list-child'))
|
||||
})
|
||||
})
|
||||
|
||||
// Covers option item wrapper behavior.
|
||||
// Covers option item rendering and prop forwarding behavior.
|
||||
describe('SelectItem', () => {
|
||||
it('should forward props and compose class names when item props are provided', () => {
|
||||
// Arrange & Act
|
||||
it('should render item text and indicator when children are provided', () => {
|
||||
// Arrange
|
||||
render(<SelectItem>Seattle</SelectItem>)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('option', { name: /seattle/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('img', { name: /selected item indicator/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward item props when item props are provided', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<SelectItem aria-label="City option" className="custom-item-class" data-testid="city-option-item">
|
||||
<SelectItem aria-label="City option" disabled>
|
||||
Seattle
|
||||
</SelectItem>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const item = screen.getByTestId('city-option-item')
|
||||
const item = screen.getByRole('option', { name: /city option/i })
|
||||
expect(item).toHaveAttribute('aria-label', 'City option')
|
||||
expect(item).toHaveAttribute('data-testid', 'city-option-item')
|
||||
expect(item).toHaveClass('h-8')
|
||||
expect(item).toHaveClass('custom-item-class')
|
||||
})
|
||||
|
||||
it('should render item text and indicator when children are provided', () => {
|
||||
// Arrange & Act
|
||||
render(<SelectItem>Seattle</SelectItem>)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('base-select-item-text')).toHaveTextContent('Seattle')
|
||||
expect(screen.getByTestId('base-select-item-indicator')).toBeInTheDocument()
|
||||
expect(item).toHaveAttribute('disabled')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,82 +1,79 @@
|
|||
import type { Placement } from '@floating-ui/react'
|
||||
import type { HTMLAttributes, ReactNode } from 'react'
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../index'
|
||||
|
||||
type MockPortalProps = {
|
||||
children: ReactNode
|
||||
type ParsedPlacement = {
|
||||
side: 'top' | 'bottom' | 'left' | 'right'
|
||||
align: 'start' | 'center' | 'end'
|
||||
}
|
||||
|
||||
type MockPositionerProps = {
|
||||
children: ReactNode
|
||||
side: string
|
||||
align: string
|
||||
sideOffset: number
|
||||
alignOffset: number
|
||||
className?: string
|
||||
type PositionerMockProps = ComponentPropsWithoutRef<'div'> & {
|
||||
side?: ParsedPlacement['side']
|
||||
align?: ParsedPlacement['align']
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
}
|
||||
|
||||
type MockPopupProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
const positionerPropsSpy = vi.fn<(props: PositionerMockProps) => void>()
|
||||
const popupPropsSpy = vi.fn<(props: ComponentPropsWithoutRef<'div'>) => void>()
|
||||
const parsePlacementMock = vi.fn<(placement: Placement) => ParsedPlacement>()
|
||||
|
||||
vi.mock('@/app/components/base/ui/placement', () => ({
|
||||
parsePlacement: vi.fn(),
|
||||
parsePlacement: (placement: Placement) => parsePlacementMock(placement),
|
||||
}))
|
||||
|
||||
vi.mock('@base-ui/react/tooltip', () => ({
|
||||
Tooltip: {
|
||||
Portal: ({ children }: MockPortalProps) => (
|
||||
<div data-testid="tooltip-portal">{children}</div>
|
||||
),
|
||||
Positioner: ({
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
sideOffset,
|
||||
alignOffset,
|
||||
className,
|
||||
}: MockPositionerProps) => (
|
||||
<div
|
||||
data-testid="tooltip-positioner"
|
||||
data-side={side}
|
||||
data-align={align}
|
||||
data-side-offset={String(sideOffset)}
|
||||
data-align-offset={String(alignOffset)}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Popup: ({ children, className, ...props }: MockPopupProps) => (
|
||||
<div data-testid="tooltip-popup" className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Provider: ({ children }: MockPortalProps) => (
|
||||
<div data-testid="tooltip-provider">{children}</div>
|
||||
),
|
||||
Root: ({ children }: MockPortalProps) => (
|
||||
<div data-testid="tooltip-root">{children}</div>
|
||||
),
|
||||
Trigger: ({ children }: MockPortalProps) => (
|
||||
<button data-testid="tooltip-trigger" type="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
}))
|
||||
vi.mock('@base-ui/react/tooltip', () => {
|
||||
const Portal = ({ children }: { children?: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
)
|
||||
|
||||
const mockParsePlacement = vi.mocked(parsePlacement)
|
||||
const Positioner = ({ children, ...props }: PositionerMockProps) => {
|
||||
positionerPropsSpy(props)
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
const Popup = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
popupPropsSpy({ className, ...props })
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Provider = ({ children }: { children?: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
)
|
||||
|
||||
const Root = ({ children }: { children?: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
)
|
||||
|
||||
const Trigger = ({ children }: { children?: ReactNode }) => (
|
||||
<button type="button">
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
return {
|
||||
Tooltip: {
|
||||
Portal,
|
||||
Positioner,
|
||||
Popup,
|
||||
Provider,
|
||||
Root,
|
||||
Trigger,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('TooltipContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockParsePlacement.mockReturnValue({ side: 'top', align: 'center' })
|
||||
parsePlacementMock.mockReturnValue({ side: 'top', align: 'center' })
|
||||
})
|
||||
|
||||
describe('Placement and offsets', () => {
|
||||
|
|
@ -85,19 +82,22 @@ describe('TooltipContent', () => {
|
|||
render(<TooltipContent>Tooltip body</TooltipContent>)
|
||||
|
||||
// Act
|
||||
const positioner = screen.getByTestId('tooltip-positioner')
|
||||
const positionerProps = positionerPropsSpy.mock.calls.at(-1)?.[0]
|
||||
|
||||
// Assert
|
||||
expect(mockParsePlacement).toHaveBeenCalledWith('top')
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'center')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '8')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '0')
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith('top')
|
||||
expect(positionerProps).toEqual(expect.objectContaining({
|
||||
side: 'top',
|
||||
align: 'center',
|
||||
sideOffset: 8,
|
||||
alignOffset: 0,
|
||||
}))
|
||||
expect(screen.getByText('Tooltip body')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use parsed placement and custom offsets when placement props are provided', () => {
|
||||
// Arrange
|
||||
mockParsePlacement.mockReturnValue({ side: 'bottom', align: 'start' })
|
||||
parsePlacementMock.mockReturnValue({ side: 'bottom', align: 'start' })
|
||||
const customPlacement: Placement = 'bottom-start'
|
||||
|
||||
// Act
|
||||
|
|
@ -107,65 +107,46 @@ describe('TooltipContent', () => {
|
|||
sideOffset={16}
|
||||
alignOffset={6}
|
||||
>
|
||||
Tooltip body
|
||||
Custom tooltip body
|
||||
</TooltipContent>,
|
||||
)
|
||||
const positioner = screen.getByTestId('tooltip-positioner')
|
||||
const positionerProps = positionerPropsSpy.mock.calls.at(-1)?.[0]
|
||||
|
||||
// Assert
|
||||
expect(mockParsePlacement).toHaveBeenCalledWith(customPlacement)
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
expect(positioner).toHaveAttribute('data-side-offset', '16')
|
||||
expect(positioner).toHaveAttribute('data-align-offset', '6')
|
||||
expect(parsePlacementMock).toHaveBeenCalledWith(customPlacement)
|
||||
expect(positionerProps).toEqual(expect.objectContaining({
|
||||
side: 'bottom',
|
||||
align: 'start',
|
||||
sideOffset: 16,
|
||||
alignOffset: 6,
|
||||
}))
|
||||
expect(screen.getByText('Custom tooltip body')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Class behavior', () => {
|
||||
it('should merge the positioner className with wrapper base class', () => {
|
||||
describe('Variant behavior', () => {
|
||||
it('should compute a different popup presentation contract for plain variant than default', () => {
|
||||
// Arrange
|
||||
render(<TooltipContent className="custom-positioner">Tooltip body</TooltipContent>)
|
||||
|
||||
// Act
|
||||
const positioner = screen.getByTestId('tooltip-positioner')
|
||||
|
||||
// Assert
|
||||
expect(positioner).toHaveClass('outline-none')
|
||||
expect(positioner).toHaveClass('custom-positioner')
|
||||
})
|
||||
|
||||
it('should apply default variant popup classes and merge popupClassName when variant is default', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<TooltipContent popupClassName="custom-popup">
|
||||
Tooltip body
|
||||
const { rerender } = render(
|
||||
<TooltipContent variant="default">
|
||||
Default tooltip body
|
||||
</TooltipContent>,
|
||||
)
|
||||
const defaultPopupProps = popupPropsSpy.mock.calls.at(-1)?.[0]
|
||||
|
||||
// Act
|
||||
const popup = screen.getByTestId('tooltip-popup')
|
||||
|
||||
// Assert
|
||||
expect(popup.className).toContain('bg-components-panel-bg')
|
||||
expect(popup.className).toContain('rounded-md')
|
||||
expect(popup).toHaveClass('custom-popup')
|
||||
})
|
||||
|
||||
it('should avoid default variant popup classes when variant is plain', () => {
|
||||
// Arrange
|
||||
render(
|
||||
<TooltipContent variant="plain" popupClassName="plain-popup">
|
||||
Tooltip body
|
||||
rerender(
|
||||
<TooltipContent variant="plain">
|
||||
Plain tooltip body
|
||||
</TooltipContent>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const popup = screen.getByTestId('tooltip-popup')
|
||||
const plainPopupProps = popupPropsSpy.mock.calls.at(-1)?.[0]
|
||||
|
||||
// Assert
|
||||
expect(popup).toHaveClass('plain-popup')
|
||||
expect(popup.className).not.toContain('bg-components-panel-bg')
|
||||
expect(popup.className).not.toContain('rounded-md')
|
||||
expect(screen.getByText('Plain tooltip body')).toBeInTheDocument()
|
||||
expect(defaultPopupProps?.className).toBeTypeOf('string')
|
||||
expect(plainPopupProps?.className).toBeTypeOf('string')
|
||||
expect(plainPopupProps?.className).not.toBe(defaultPopupProps?.className)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -184,9 +165,16 @@ describe('TooltipContent', () => {
|
|||
)
|
||||
|
||||
// Act
|
||||
const popup = screen.getByTestId('tooltip-popup')
|
||||
const popup = screen.getByRole('tooltip', { name: 'help text' })
|
||||
const popupProps = popupPropsSpy.mock.calls.at(-1)?.[0]
|
||||
|
||||
// Assert
|
||||
expect(popupProps).toEqual(expect.objectContaining({
|
||||
'id': 'popup-id',
|
||||
'role': 'tooltip',
|
||||
'aria-label': 'help text',
|
||||
'data-track-id': 'tooltip-track',
|
||||
}))
|
||||
expect(popup).toHaveAttribute('id', 'popup-id')
|
||||
expect(popup).toHaveAttribute('role', 'tooltip')
|
||||
expect(popup).toHaveAttribute('aria-label', 'help text')
|
||||
|
|
|
|||
Loading…
Reference in New Issue