mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 11:10:19 +08:00
test(web): add high-quality unit tests for Base UI wrapper primitives (#32904)
This commit is contained in:
parent
7f67e1a2fc
commit
3a8ff301fc
70
web/app/components/base/ui/dialog/__tests__/index.spec.tsx
Normal file
70
web/app/components/base/ui/dialog/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../index'
|
||||
|
||||
describe('Dialog wrapper', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render dialog content when dialog is open', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogContent>
|
||||
</Dialog>,
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog).toHaveTextContent('Dialog Title')
|
||||
expect(dialog).toHaveTextContent('Dialog Description')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should not render close button when closable is omitted', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<span>Dialog body</span>
|
||||
</DialogContent>
|
||||
</Dialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button when closable is true', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent closable>
|
||||
<span>Dialog body</span>
|
||||
</DialogContent>
|
||||
</Dialog>,
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' })
|
||||
|
||||
expect(dialog).toContainElement(closeButton)
|
||||
expect(closeButton).toHaveAttribute('aria-label', 'Close')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Exports', () => {
|
||||
it('should map dialog aliases to the matching base dialog primitives', () => {
|
||||
expect(Dialog).toBe(BaseDialog.Root)
|
||||
expect(DialogTrigger).toBe(BaseDialog.Trigger)
|
||||
expect(DialogTitle).toBe(BaseDialog.Title)
|
||||
expect(DialogDescription).toBe(BaseDialog.Description)
|
||||
expect(DialogClose).toBe(BaseDialog.Close)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,294 @@
|
||||
import { Menu } from '@base-ui/react/menu'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '../index'
|
||||
|
||||
describe('dropdown-menu wrapper', () => {
|
||||
describe('alias exports', () => {
|
||||
it('should map direct aliases to the corresponding Menu primitive when importing menu roots', () => {
|
||||
expect(DropdownMenu).toBe(Menu.Root)
|
||||
expect(DropdownMenuPortal).toBe(Menu.Portal)
|
||||
expect(DropdownMenuTrigger).toBe(Menu.Trigger)
|
||||
expect(DropdownMenuSub).toBe(Menu.SubmenuRoot)
|
||||
expect(DropdownMenuGroup).toBe(Menu.Group)
|
||||
expect(DropdownMenuRadioGroup).toBe(Menu.RadioGroup)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropdownMenuContent', () => {
|
||||
it('should position content at bottom-end with default placement when props are omitted', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent positionerProps={{ 'role': 'group', 'aria-label': 'content positioner' }}>
|
||||
<DropdownMenuItem>Content action</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'content positioner' })
|
||||
const popup = screen.getByRole('menu')
|
||||
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(within(popup).getByRole('menuitem', { name: 'Content action' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom placement when custom positioning props are provided', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="top-start"
|
||||
sideOffset={12}
|
||||
alignOffset={-3}
|
||||
positionerProps={{ 'role': 'group', 'aria-label': 'custom content positioner' }}
|
||||
>
|
||||
<DropdownMenuItem>Custom content</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'custom content positioner' })
|
||||
const popup = screen.getByRole('menu')
|
||||
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
expect(within(popup).getByRole('menuitem', { name: 'Custom content' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward passthrough attributes and handlers when positionerProps and popupProps are provided', () => {
|
||||
const handlePositionerMouseEnter = vi.fn()
|
||||
const handlePopupClick = vi.fn()
|
||||
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'dropdown content positioner',
|
||||
'id': 'dropdown-content-positioner',
|
||||
'onMouseEnter': handlePositionerMouseEnter,
|
||||
}}
|
||||
popupProps={{
|
||||
role: 'menu',
|
||||
id: 'dropdown-content-popup',
|
||||
onClick: handlePopupClick,
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem>Passthrough content</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'dropdown content positioner' })
|
||||
const popup = screen.getByRole('menu')
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropdownMenuSubContent', () => {
|
||||
it('should position sub-content at left-start with default placement when props are omitted', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub open>
|
||||
<DropdownMenuSubTrigger>More actions</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent positionerProps={{ 'role': 'group', 'aria-label': 'sub positioner' }}>
|
||||
<DropdownMenuItem>Sub action</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'sub positioner' })
|
||||
expect(positioner).toHaveAttribute('data-side', 'left')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
expect(screen.getByRole('menuitem', { name: 'Sub action' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom placement and forward passthrough props for sub-content when custom props are provided', () => {
|
||||
const handlePositionerFocus = vi.fn()
|
||||
const handlePopupClick = vi.fn()
|
||||
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub open>
|
||||
<DropdownMenuSubTrigger>More actions</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
placement="right-end"
|
||||
sideOffset={6}
|
||||
alignOffset={2}
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'dropdown sub positioner',
|
||||
'id': 'dropdown-sub-positioner',
|
||||
'onFocus': handlePositionerFocus,
|
||||
}}
|
||||
popupProps={{
|
||||
role: 'menu',
|
||||
id: 'dropdown-sub-popup',
|
||||
onClick: handlePopupClick,
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem>Custom sub action</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'dropdown sub positioner' })
|
||||
const popup = screen.getByRole('menu', { name: 'More actions' })
|
||||
fireEvent.focus(positioner)
|
||||
fireEvent.click(popup)
|
||||
|
||||
expect(positioner).toHaveAttribute('data-side', 'right')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(positioner).toHaveAttribute('id', 'dropdown-sub-positioner')
|
||||
expect(popup).toHaveAttribute('id', 'dropdown-sub-popup')
|
||||
expect(handlePositionerFocus).toHaveBeenCalledTimes(1)
|
||||
expect(handlePopupClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropdownMenuSubTrigger', () => {
|
||||
it('should render submenu trigger content when trigger children are provided', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub open>
|
||||
<DropdownMenuSubTrigger>Trigger item</DropdownMenuSubTrigger>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'Trigger item' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([true, false])('should remain interactive and not leak destructive prop when destructive is %s', (destructive) => {
|
||||
const handleClick = vi.fn()
|
||||
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSub open>
|
||||
<DropdownMenuSubTrigger
|
||||
destructive={destructive}
|
||||
aria-label="submenu action"
|
||||
id={`submenu-trigger-${String(destructive)}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Trigger item
|
||||
</DropdownMenuSubTrigger>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropdownMenuItem', () => {
|
||||
it.each([true, false])('should remain interactive and not leak destructive prop when destructive is %s', (destructive) => {
|
||||
const handleClick = vi.fn()
|
||||
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
destructive={destructive}
|
||||
aria-label="menu action"
|
||||
id={`menu-item-${String(destructive)}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Item label
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
const item = screen.getByRole('menuitem', { name: 'menu action' })
|
||||
fireEvent.click(item)
|
||||
|
||||
expect(item).toHaveAttribute('id', `menu-item-${String(destructive)}`)
|
||||
expect(item).not.toHaveAttribute('destructive')
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropdownMenuSeparator', () => {
|
||||
it('should forward passthrough props and handlers when separator props are provided', () => {
|
||||
const handleMouseEnter = vi.fn()
|
||||
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSeparator
|
||||
aria-label="actions divider"
|
||||
id="menu-separator"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
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', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>First action</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Second action</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'First action' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'Second action' })).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('separator')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
107
web/app/components/base/ui/popover/__tests__/index.spec.tsx
Normal file
107
web/app/components/base/ui/popover/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { Popover as BasePopover } from '@base-ui/react/popover'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
Popover,
|
||||
PopoverClose,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from '..'
|
||||
|
||||
describe('PopoverContent', () => {
|
||||
describe('Placement', () => {
|
||||
it('should use bottom placement and default offsets when placement props are not provided', () => {
|
||||
render(
|
||||
<Popover open>
|
||||
<PopoverTrigger aria-label="popover trigger">Open</PopoverTrigger>
|
||||
<PopoverContent
|
||||
positionerProps={{ 'role': 'group', 'aria-label': 'default positioner' }}
|
||||
popupProps={{ 'role': 'dialog', 'aria-label': 'default popover' }}
|
||||
>
|
||||
<span>Default content</span>
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'default positioner' })
|
||||
const popup = screen.getByRole('dialog', { name: 'default popover' })
|
||||
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'center')
|
||||
expect(popup).toHaveTextContent('Default content')
|
||||
})
|
||||
|
||||
it('should apply parsed custom placement and custom offsets when placement props are provided', () => {
|
||||
render(
|
||||
<Popover open>
|
||||
<PopoverTrigger aria-label="popover trigger">Open</PopoverTrigger>
|
||||
<PopoverContent
|
||||
placement="top-end"
|
||||
sideOffset={14}
|
||||
alignOffset={6}
|
||||
positionerProps={{ 'role': 'group', 'aria-label': 'custom positioner' }}
|
||||
popupProps={{ 'role': 'dialog', 'aria-label': 'custom popover' }}
|
||||
>
|
||||
<span>Custom placement content</span>
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'custom positioner' })
|
||||
const popup = screen.getByRole('dialog', { name: 'custom popover' })
|
||||
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
expect(popup).toHaveTextContent('Custom placement content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Passthrough props', () => {
|
||||
it('should forward positionerProps and popupProps when passthrough props are provided', () => {
|
||||
const onPopupClick = vi.fn()
|
||||
|
||||
render(
|
||||
<Popover open>
|
||||
<PopoverTrigger aria-label="popover trigger">Open</PopoverTrigger>
|
||||
<PopoverContent
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'popover positioner',
|
||||
'id': 'popover-positioner-id',
|
||||
}}
|
||||
popupProps={{
|
||||
'id': 'popover-popup-id',
|
||||
'role': 'dialog',
|
||||
'aria-label': 'popover content',
|
||||
'onClick': onPopupClick,
|
||||
}}
|
||||
>
|
||||
<span>Popover body</span>
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'popover positioner' })
|
||||
const popup = screen.getByRole('dialog', { name: 'popover content' })
|
||||
fireEvent.click(popup)
|
||||
|
||||
expect(positioner).toHaveAttribute('id', 'popover-positioner-id')
|
||||
expect(popup).toHaveAttribute('id', 'popover-popup-id')
|
||||
expect(onPopupClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Popover aliases', () => {
|
||||
describe('Export mapping', () => {
|
||||
it('should map aliases to the matching base popover primitives when wrapper exports are imported', () => {
|
||||
expect(Popover).toBe(BasePopover.Root)
|
||||
expect(PopoverTrigger).toBe(BasePopover.Trigger)
|
||||
expect(PopoverClose).toBe(BasePopover.Close)
|
||||
expect(PopoverTitle).toBe(BasePopover.Title)
|
||||
expect(PopoverDescription).toBe(BasePopover.Description)
|
||||
})
|
||||
})
|
||||
})
|
||||
219
web/app/components/base/ui/select/__tests__/index.spec.tsx
Normal file
219
web/app/components/base/ui/select/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../index'
|
||||
|
||||
const renderOpenSelect = ({
|
||||
triggerProps = {},
|
||||
contentProps = {},
|
||||
onValueChange,
|
||||
}: {
|
||||
triggerProps?: Record<string, unknown>
|
||||
contentProps?: Record<string, unknown>
|
||||
onValueChange?: (value: string | null) => void
|
||||
} = {}) => {
|
||||
return render(
|
||||
<Select open defaultValue="seattle" onValueChange={onValueChange}>
|
||||
<SelectTrigger aria-label="city select" {...triggerProps}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'select positioner',
|
||||
}}
|
||||
popupProps={{
|
||||
'role': 'dialog',
|
||||
'aria-label': 'select popup',
|
||||
}}
|
||||
listProps={{
|
||||
'role': 'listbox',
|
||||
'aria-label': 'select list',
|
||||
}}
|
||||
{...contentProps}
|
||||
>
|
||||
<SelectItem value="seattle">Seattle</SelectItem>
|
||||
<SelectItem value="new-york">New York</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Select wrappers', () => {
|
||||
describe('SelectTrigger', () => {
|
||||
it('should render clear button when clearable is true and loading is false', () => {
|
||||
renderOpenSelect({
|
||||
triggerProps: { clearable: true },
|
||||
})
|
||||
|
||||
expect(screen.getByRole('button', { name: /clear selection/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide clear button when loading is true', () => {
|
||||
renderOpenSelect({
|
||||
triggerProps: { clearable: true, loading: true },
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('button', { name: /clear selection/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward native trigger props when trigger props are provided', () => {
|
||||
renderOpenSelect({
|
||||
triggerProps: {
|
||||
'aria-label': 'Choose option',
|
||||
'disabled': true,
|
||||
},
|
||||
})
|
||||
|
||||
const trigger = screen.getByRole('combobox', { name: 'Choose option' })
|
||||
expect(trigger).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should call onClear and stop click propagation when clear button is clicked', () => {
|
||||
const onClear = vi.fn()
|
||||
const onTriggerClick = vi.fn()
|
||||
|
||||
renderOpenSelect({
|
||||
triggerProps: {
|
||||
clearable: true,
|
||||
onClear,
|
||||
onClick: onTriggerClick,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /clear selection/i }))
|
||||
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
expect(onTriggerClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should stop mouse down propagation when clear button receives mouse down', () => {
|
||||
const onTriggerMouseDown = vi.fn()
|
||||
|
||||
renderOpenSelect({
|
||||
triggerProps: {
|
||||
clearable: true,
|
||||
onMouseDown: onTriggerMouseDown,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.mouseDown(screen.getByRole('button', { name: /clear selection/i }))
|
||||
|
||||
expect(onTriggerMouseDown).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not throw when clear button is clicked without onClear handler', () => {
|
||||
renderOpenSelect({
|
||||
triggerProps: { clearable: true },
|
||||
})
|
||||
|
||||
const clearButton = screen.getByRole('button', { name: /clear selection/i })
|
||||
expect(() => fireEvent.click(clearButton)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SelectContent', () => {
|
||||
it('should use default placement when placement is not provided', () => {
|
||||
renderOpenSelect()
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'select positioner' })
|
||||
expect(positioner).toHaveAttribute('data-side', 'bottom')
|
||||
expect(positioner).toHaveAttribute('data-align', 'start')
|
||||
})
|
||||
|
||||
it('should apply custom placement when placement props are provided', () => {
|
||||
renderOpenSelect({
|
||||
contentProps: {
|
||||
placement: 'top-end',
|
||||
sideOffset: 12,
|
||||
alignOffset: 6,
|
||||
},
|
||||
})
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'select positioner' })
|
||||
expect(positioner).toHaveAttribute('data-side', 'top')
|
||||
expect(positioner).toHaveAttribute('data-align', 'end')
|
||||
})
|
||||
|
||||
it('should forward passthrough props to positioner popup and list when passthrough props are provided', () => {
|
||||
const onPositionerMouseEnter = vi.fn()
|
||||
const onPopupClick = vi.fn()
|
||||
const onListFocus = vi.fn()
|
||||
|
||||
render(
|
||||
<Select open defaultValue="seattle">
|
||||
<SelectTrigger aria-label="city select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
positionerProps={{
|
||||
'role': 'group',
|
||||
'aria-label': 'select positioner',
|
||||
'id': 'select-positioner',
|
||||
'onMouseEnter': onPositionerMouseEnter,
|
||||
}}
|
||||
popupProps={{
|
||||
'role': 'dialog',
|
||||
'aria-label': 'select popup',
|
||||
'id': 'select-popup',
|
||||
'onClick': onPopupClick,
|
||||
}}
|
||||
listProps={{
|
||||
'role': 'listbox',
|
||||
'aria-label': 'select list',
|
||||
'id': 'select-list',
|
||||
'onFocus': onListFocus,
|
||||
}}
|
||||
>
|
||||
<SelectItem value="seattle">Seattle</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
|
||||
const positioner = screen.getByRole('group', { name: 'select positioner' })
|
||||
const popup = screen.getByRole('dialog', { name: 'select popup' })
|
||||
const list = screen.getByRole('listbox', { name: 'select list' })
|
||||
|
||||
fireEvent.mouseEnter(positioner)
|
||||
fireEvent.click(popup)
|
||||
fireEvent.focus(list)
|
||||
|
||||
expect(positioner).toHaveAttribute('id', 'select-positioner')
|
||||
expect(popup).toHaveAttribute('id', 'select-popup')
|
||||
expect(list).toHaveAttribute('id', 'select-list')
|
||||
expect(onPositionerMouseEnter).toHaveBeenCalledTimes(1)
|
||||
expect(onPopupClick).toHaveBeenCalledTimes(1)
|
||||
expect(onListFocus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SelectItem', () => {
|
||||
it('should render options when children are provided', () => {
|
||||
renderOpenSelect()
|
||||
|
||||
expect(screen.getByRole('option', { name: 'Seattle' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: 'New York' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not call onValueChange when disabled item is clicked', () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
render(
|
||||
<Select open defaultValue="seattle" onValueChange={onValueChange}>
|
||||
<SelectTrigger aria-label="city select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listProps={{ 'role': 'listbox', 'aria-label': 'select list' }}>
|
||||
<SelectItem value="seattle">Seattle</SelectItem>
|
||||
<SelectItem value="new-york" disabled aria-label="Disabled New York">
|
||||
New York
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('option', { name: 'Disabled New York' }))
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
95
web/app/components/base/ui/tooltip/__tests__/index.spec.tsx
Normal file
95
web/app/components/base/ui/tooltip/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../index'
|
||||
|
||||
describe('TooltipContent', () => {
|
||||
describe('Placement and offsets', () => {
|
||||
it('should use default top placement when placement is not provided', () => {
|
||||
render(
|
||||
<Tooltip open>
|
||||
<TooltipTrigger aria-label="tooltip trigger">Trigger</TooltipTrigger>
|
||||
<TooltipContent role="tooltip" aria-label="default tooltip">
|
||||
Tooltip body
|
||||
</TooltipContent>
|
||||
</Tooltip>,
|
||||
)
|
||||
|
||||
const popup = screen.getByRole('tooltip', { name: 'default tooltip' })
|
||||
expect(popup).toHaveAttribute('data-side', 'top')
|
||||
expect(popup).toHaveAttribute('data-align', 'center')
|
||||
expect(popup).toHaveTextContent('Tooltip body')
|
||||
})
|
||||
|
||||
it('should apply custom placement when placement props are provided', () => {
|
||||
render(
|
||||
<Tooltip open>
|
||||
<TooltipTrigger aria-label="tooltip trigger">Trigger</TooltipTrigger>
|
||||
<TooltipContent
|
||||
placement="bottom-start"
|
||||
sideOffset={16}
|
||||
alignOffset={6}
|
||||
role="tooltip"
|
||||
aria-label="custom tooltip"
|
||||
>
|
||||
Custom tooltip body
|
||||
</TooltipContent>
|
||||
</Tooltip>,
|
||||
)
|
||||
|
||||
const popup = screen.getByRole('tooltip', { name: 'custom tooltip' })
|
||||
expect(popup).toHaveAttribute('data-side', 'bottom')
|
||||
expect(popup).toHaveAttribute('data-align', 'start')
|
||||
expect(popup).toHaveTextContent('Custom tooltip body')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Variant and popup props', () => {
|
||||
it('should render popup content when variant is plain', () => {
|
||||
render(
|
||||
<Tooltip open>
|
||||
<TooltipTrigger aria-label="tooltip trigger">Trigger</TooltipTrigger>
|
||||
<TooltipContent variant="plain" role="tooltip" aria-label="plain tooltip">
|
||||
Plain tooltip body
|
||||
</TooltipContent>
|
||||
</Tooltip>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('tooltip', { name: 'plain tooltip' })).toHaveTextContent('Plain tooltip body')
|
||||
})
|
||||
|
||||
it('should forward popup props and handlers when popup props are provided', () => {
|
||||
const onMouseEnter = vi.fn()
|
||||
|
||||
render(
|
||||
<Tooltip open>
|
||||
<TooltipTrigger aria-label="tooltip trigger">Trigger</TooltipTrigger>
|
||||
<TooltipContent
|
||||
id="tooltip-popup-id"
|
||||
role="tooltip"
|
||||
aria-label="help text"
|
||||
data-track-id="tooltip-track"
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
Tooltip body
|
||||
</TooltipContent>
|
||||
</Tooltip>,
|
||||
)
|
||||
|
||||
const popup = screen.getByRole('tooltip', { name: 'help text' })
|
||||
fireEvent.mouseEnter(popup)
|
||||
|
||||
expect(popup).toHaveAttribute('id', 'tooltip-popup-id')
|
||||
expect(popup).toHaveAttribute('data-track-id', 'tooltip-track')
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tooltip aliases', () => {
|
||||
it('should map alias exports to BaseTooltip components when wrapper exports are imported', () => {
|
||||
expect(TooltipProvider).toBe(BaseTooltip.Provider)
|
||||
expect(Tooltip).toBe(BaseTooltip.Root)
|
||||
expect(TooltipTrigger).toBe(BaseTooltip.Trigger)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user