From 3a8ff301fca403e30435b971f7082f809b760ca1 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Tue, 3 Mar 2026 18:21:33 +0800
Subject: [PATCH] test(web): add high-quality unit tests for Base UI wrapper
primitives (#32904)
---
.../base/ui/dialog/__tests__/index.spec.tsx | 70 +++++
.../ui/dropdown-menu/__tests__/index.spec.tsx | 294 ++++++++++++++++++
.../base/ui/popover/__tests__/index.spec.tsx | 107 +++++++
.../base/ui/select/__tests__/index.spec.tsx | 219 +++++++++++++
.../base/ui/tooltip/__tests__/index.spec.tsx | 95 ++++++
5 files changed, 785 insertions(+)
create mode 100644 web/app/components/base/ui/dialog/__tests__/index.spec.tsx
create mode 100644 web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx
create mode 100644 web/app/components/base/ui/popover/__tests__/index.spec.tsx
create mode 100644 web/app/components/base/ui/select/__tests__/index.spec.tsx
create mode 100644 web/app/components/base/ui/tooltip/__tests__/index.spec.tsx
diff --git a/web/app/components/base/ui/dialog/__tests__/index.spec.tsx b/web/app/components/base/ui/dialog/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..0861fff603
--- /dev/null
+++ b/web/app/components/base/ui/dialog/__tests__/index.spec.tsx
@@ -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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
+ })
+
+ it('should render close button when closable is true', () => {
+ render(
+ ,
+ )
+
+ 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)
+ })
+ })
+})
diff --git a/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx b/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..b381078180
--- /dev/null
+++ b/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx
@@ -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(
+
+ Open
+
+ Content action
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+ Custom content
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+ Passthrough content
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+ More actions
+
+ Sub action
+
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+ More actions
+
+ Custom sub action
+
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+ Trigger item
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+ First action
+
+ Second action
+
+ ,
+ )
+
+ expect(screen.getByRole('menuitem', { name: 'First action' })).toBeInTheDocument()
+ expect(screen.getByRole('menuitem', { name: 'Second action' })).toBeInTheDocument()
+ expect(screen.getAllByRole('separator')).toHaveLength(1)
+ })
+ })
+})
diff --git a/web/app/components/base/ui/popover/__tests__/index.spec.tsx b/web/app/components/base/ui/popover/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..9d65f8c934
--- /dev/null
+++ b/web/app/components/base/ui/popover/__tests__/index.spec.tsx
@@ -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(
+
+ Open
+
+ Default content
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+ Custom placement content
+
+ ,
+ )
+
+ 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(
+
+ Open
+
+ Popover body
+
+ ,
+ )
+
+ 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)
+ })
+ })
+})
diff --git a/web/app/components/base/ui/select/__tests__/index.spec.tsx b/web/app/components/base/ui/select/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..7744a0e22c
--- /dev/null
+++ b/web/app/components/base/ui/select/__tests__/index.spec.tsx
@@ -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
+ contentProps?: Record
+ onValueChange?: (value: string | null) => void
+} = {}) => {
+ return render(
+ ,
+ )
+}
+
+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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ fireEvent.click(screen.getByRole('option', { name: 'Disabled New York' }))
+
+ expect(onValueChange).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx b/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..4582f07cbe
--- /dev/null
+++ b/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx
@@ -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(
+
+ Trigger
+
+ Tooltip body
+
+ ,
+ )
+
+ 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(
+
+ Trigger
+
+ Custom tooltip body
+
+ ,
+ )
+
+ 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(
+
+ Trigger
+
+ Plain tooltip body
+
+ ,
+ )
+
+ 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(
+
+ Trigger
+
+ ,
+ )
+
+ 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)
+ })
+})