From ec70e7c82f594a1f94407ef2773b0976e4175f1b Mon Sep 17 00:00:00 2001 From: yyh Date: Tue, 3 Mar 2026 17:01:17 +0800 Subject: [PATCH] fix: update tests --- .../base/ui/dialog/__tests__/index.spec.tsx | 127 ++++---- .../ui/dropdown-menu/__tests__/index.spec.tsx | 297 ++++++++++-------- .../base/ui/popover/__tests__/index.spec.tsx | 225 ++++++------- .../base/ui/select/__tests__/index.spec.tsx | 214 +++++++------ .../base/ui/tooltip/__tests__/index.spec.tsx | 212 ++++++------- 5 files changed, 546 insertions(+), 529 deletions(-) diff --git a/web/app/components/base/ui/dialog/__tests__/index.spec.tsx b/web/app/components/base/ui/dialog/__tests__/index.spec.tsx index eb8cb179a6..1c9cb5d12f 100644 --- a/web/app/components/base/ui/dialog/__tests__/index.spec.tsx +++ b/web/app/components/base/ui/dialog/__tests__/index.spec.tsx @@ -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) => ( -
+ const createDivPrimitive = () => { + return vi.fn(({ children, ...props }: DivPrimitiveProps) => ( +
{children}
)) } + const createButtonPrimitive = () => { + return vi.fn(({ children, ...props }: ButtonPrimitiveProps) => ( + + )) + } + + const createPopupPrimitive = () => { + return vi.fn(({ children, ...props }: SectionPrimitiveProps) => ( +
+ {children} +
+ )) + } + 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( - - content - , - ) - - // 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( - + content , ) - 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( - -
{childText}
+ + content , ) - 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( + + content + , + ) // 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) }) }) 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 index f0c0a0d019..a11634a6ef 100644 --- a/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx +++ b/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx @@ -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(({ children, ...props }, ref) => { - return React.createElement('div', { ref, 'data-testid': testId, ...props }, children) + const createPrimitive = ({ displayName, defaultRole }: PrimitiveOptions) => { + const Primitive = React.forwardRef(({ 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(({ 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(({ 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( - content child + , ) // 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" > custom content , ) // 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( passthrough content @@ -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( - sub content child + , ) // 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, + }} > custom sub content , ) // 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( - - passthrough sub content - , - ) - - // 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( - + Trigger item , ) // 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( - + Trigger item , ) // 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( - + Item label , ) // 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( - - Item label - , - ) - - // 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( + , + ) + + // 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() + render( + <> + First action + + Second action + , + ) // 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) }) }) }) diff --git a/web/app/components/base/ui/popover/__tests__/index.spec.tsx b/web/app/components/base/ui/popover/__tests__/index.spec.tsx index c509d0eefb..9712bc2589 100644 --- a/web/app/components/base/ui/popover/__tests__/index.spec.tsx +++ b/web/app/components/base/ui/popover/__tests__/index.spec.tsx @@ -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'>) => ( -
{children}
+ const Root = ({ children, ...props }: PrimitiveProps) => ( +
+ {children} +
) const Trigger = ({ children, ...props }: ComponentPropsWithoutRef<'button'>) => ( - + ) const Close = ({ children, ...props }: ComponentPropsWithoutRef<'button'>) => ( - + ) const Title = ({ children, ...props }: ComponentPropsWithoutRef<'h2'>) => ( -

{children}

+

+ {children} +

) const Description = ({ children, ...props }: ComponentPropsWithoutRef<'p'>) => ( -

{children}

+

+ {children} +

) - const Portal = ({ children }: { children?: ReactNode }) => ( -
{children}
+ const Portal = ({ children }: PrimitiveProps) => ( +
{children}
) - const Positioner = ({ children, ...props }: PositionerMockProps) => { - positionerPropsSpy(props) - return ( -
- {children} -
- ) - } + const Positioner = ({ + children, + side, + align, + sideOffset, + alignOffset, + ...props + }: PositionerProps) => ( +
+ {children} +
+ ) - const Popup = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => { - popupClassNameSpy(className) - popupPropsSpy({ className, ...props }) - return ( -
- {children} -
- ) - } + const Popup = ({ children, ...props }: PrimitiveProps) => ( +
+ {children} +
+ ) 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( - + Default content , ) // 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( - - Parsed content - , - ) - - // 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( - - Styled content + + Custom placement content , ) // 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( - - - , - ) + 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) }) }) }) diff --git a/web/app/components/base/ui/select/__tests__/index.spec.tsx b/web/app/components/base/ui/select/__tests__/index.spec.tsx index ca7586c7e7..c520ca4f44 100644 --- a/web/app/components/base/ui/select/__tests__/index.spec.tsx +++ b/web/app/components/base/ui/select/__tests__/index.spec.tsx @@ -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) =>
{children}
- const Value = ({ children }: WithChildren) => {children} - const Group = ({ children }: WithChildren) =>
{children}
- const GroupLabel = ({ children }: WithChildren) =>
{children}
- const Separator = (props: HTMLAttributes) =>
+ const Root = ({ children }: WithChildren) =>
{children}
+ const Value = ({ children }: WithChildren) => {children} + const Group = ({ children }: WithChildren) =>
{children}
+ const GroupLabel = ({ children }: WithChildren) =>
{children}
+ const Separator = (props: HTMLAttributes) =>
const Trigger = ({ children, ...props }: ButtonHTMLAttributes) => ( - ) const Icon = ({ children, ...props }: HTMLAttributes) => ( - + {children} ) - const Portal = ({ children }: WithChildren) =>
{children}
+ const Portal = ({ children }: WithChildren) =>
{children}
const Positioner = ({ children, side, align, sideOffset, alignOffset, + alignItemWithTrigger, className, ...props }: PositionerProps) => (
@@ -74,28 +75,28 @@ vi.mock('@base-ui/react/select', () => {
) const Popup = ({ children, ...props }: HTMLAttributes) => ( -
+
{children}
) const List = ({ children, ...props }: HTMLAttributes) => ( -
+
{children}
) const Item = ({ children, ...props }: HTMLAttributes) => ( -
+
{children}
) const ItemText = ({ children, ...props }: HTMLAttributes) => ( - + {children} ) const ItemIndicator = ({ children, ...props }: HTMLAttributes) => ( - + {children} ) @@ -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(Trigger Label) + + // 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(Trigger Label) + + // 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(Trigger Label) + + // 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( 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(Trigger Label) + it('should call onClear and stop click propagation when clear button is clicked', () => { + // Arrange + const onClear = vi.fn() + const onTriggerClick = vi.fn() + render( + + Trigger Label + , + ) + + // 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(Trigger Label) + it('should stop mouse down propagation when clear button receives mouse down', () => { + // Arrange + const onTriggerMouseDown = vi.fn() + render( + + Trigger Label + , + ) + + // 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(Trigger Label) + 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(
Option A
) + render( + + Option A + , + ) // 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( - -
Option A
-
, - ) - - // 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(
Option A
, ) // 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( { ) // 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( - - Option A - , - ) - - // 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(Seattle) + + // 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( - + Seattle , ) // 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(Seattle) - - // Assert - expect(screen.getByTestId('base-select-item-text')).toHaveTextContent('Seattle') - expect(screen.getByTestId('base-select-item-indicator')).toBeInTheDocument() + expect(item).toHaveAttribute('disabled') }) }) }) diff --git a/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx b/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx index 3432b19eaa..4b72eb9e65 100644 --- a/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx +++ b/web/app/components/base/ui/tooltip/__tests__/index.spec.tsx @@ -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 & { - 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) => ( -
{children}
- ), - Positioner: ({ - children, - side, - align, - sideOffset, - alignOffset, - className, - }: MockPositionerProps) => ( -
- {children} -
- ), - Popup: ({ children, className, ...props }: MockPopupProps) => ( -
- {children} -
- ), - Provider: ({ children }: MockPortalProps) => ( -
{children}
- ), - Root: ({ children }: MockPortalProps) => ( -
{children}
- ), - Trigger: ({ children }: MockPortalProps) => ( - - ), - }, -})) +vi.mock('@base-ui/react/tooltip', () => { + const Portal = ({ children }: { children?: ReactNode }) => ( +
{children}
+ ) -const mockParsePlacement = vi.mocked(parsePlacement) + const Positioner = ({ children, ...props }: PositionerMockProps) => { + positionerPropsSpy(props) + return
{children}
+ } + + const Popup = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => { + popupPropsSpy({ className, ...props }) + return ( +
+ {children} +
+ ) + } + + const Provider = ({ children }: { children?: ReactNode }) => ( +
{children}
+ ) + + const Root = ({ children }: { children?: ReactNode }) => ( +
{children}
+ ) + + const Trigger = ({ children }: { children?: ReactNode }) => ( + + ) + + 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(Tooltip body) // 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 , ) - 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(Tooltip body) - - // 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( - - Tooltip body + const { rerender } = render( + + Default tooltip body , ) + 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( - - Tooltip body + rerender( + + Plain tooltip body , ) - - // 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')