test(dify-ui): remove low-value style assertions (#38418)

This commit is contained in:
yyh 2026-07-04 22:35:00 +08:00 committed by GitHub
parent 2e1ab194b7
commit a0f8347513
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 48 additions and 810 deletions

View File

@ -26,7 +26,6 @@ describe('AlertDialog wrapper', () => {
await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Confirm Delete')
await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('This action cannot be undone.')
await expect.element(document.body.querySelector('.bg-background-overlay') as HTMLElement).toHaveClass('absolute', 'inset-0', 'z-50')
})
it('should not render content when dialog is closed', async () => {
@ -96,7 +95,7 @@ describe('AlertDialog wrapper', () => {
})
describe('Composition Helpers', () => {
it('should render actions wrapper and default confirm button styles', async () => {
it('should render actions wrapper and confirm button', async () => {
const screen = await render(
<AlertDialog open>
<AlertDialogContent>
@ -108,8 +107,8 @@ describe('AlertDialog wrapper', () => {
</AlertDialog>,
)
await expect.element(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
await expect.element(screen.getByRole('button', { name: 'Confirm' })).toHaveClass('bg-components-button-destructive-primary-bg')
await expect.element(screen.getByTestId('actions')).toHaveClass('custom-actions')
await expect.element(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument()
})
it('should keep dialog open after confirm click and close via cancel helper', async () => {

View File

@ -72,28 +72,6 @@ const renderAutocomplete = ({
describe('Autocomplete wrappers', () => {
describe('Input group and input', () => {
it('should apply medium input group and input classes by default', async () => {
const screen = await renderAutocomplete()
await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-lg')
await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('px-3')
await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('system-sm-regular')
})
it('should apply large input group and input classes when large size is provided', async () => {
const screen = await renderAutocomplete({
children: (
<AutocompleteInputGroup size="large" data-testid="input-group">
<AutocompleteInput size="large" aria-label="Search suggestions" data-testid="input" />
</AutocompleteInputGroup>
),
})
await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-[10px]')
await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('px-4')
await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('system-md-regular')
})
it('should set input defaults and forward passthrough props', async () => {
const screen = await renderAutocomplete({
children: (
@ -122,8 +100,6 @@ describe('Autocomplete wrappers', () => {
await expect.element(screen.getByRole('button', { name: 'Clear autocomplete' })).toHaveAttribute('type', 'button')
await expect.element(screen.getByRole('button', { name: 'Open autocomplete suggestions' })).toHaveAttribute('type', 'button')
expect(screen.getByRole('button', { name: 'Clear autocomplete' }).element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
expect(screen.getByRole('button', { name: 'Open autocomplete suggestions' }).element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should preserve explicit labels and custom children', async () => {
@ -143,8 +119,6 @@ describe('Autocomplete wrappers', () => {
expect(screen.getByRole('button', { name: 'Reset search' }).element()).toContainElement(screen.getByTestId('custom-clear').element())
expect(screen.getByRole('button', { name: 'Show suggestions' }).element()).toContainElement(screen.getByTestId('custom-trigger').element())
expect(screen.getByRole('button', { name: 'Reset search' }).element().querySelector('.i-ri-close-line')).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Show suggestions' }).element().querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument()
})
it('should rely on aria-labelledby when provided instead of injecting fallback labels', async () => {
@ -168,15 +142,11 @@ describe('Autocomplete wrappers', () => {
})
describe('Content and options', () => {
it('should use default overlay placement and Dify popup classes', async () => {
it('should use default overlay placement', async () => {
const screen = await renderAutocomplete({ open: true })
await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-side', 'bottom')
await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-align', 'start')
await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveClass('z-50')
await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('rounded-xl')
await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('w-(--anchor-width)')
await expect.element(screen.getByRole('listbox', { name: 'autocomplete list' })).toHaveClass('scroll-py-1')
})
it('should apply custom placement side and passthrough popup props', async () => {
@ -212,17 +182,6 @@ describe('Autocomplete wrappers', () => {
expect(onPopupClick).toHaveBeenCalledTimes(1)
})
it('should render item text indicator status and empty wrappers with design classes', async () => {
const screen = await renderAutocomplete({ open: true })
await expect.element(screen.getByText('Workflow')).toHaveClass('system-sm-medium')
await expect.element(screen.getByTestId('status')).toHaveClass('text-text-tertiary')
await expect.element(screen.getByTestId('empty')).toHaveClass('system-sm-regular')
await expect.element(screen.getByTestId('empty')).toHaveClass('empty:p-0')
expect(screen.getByTestId('empty').element().getBoundingClientRect().height).toBe(0)
expect(screen.getByText('Workflow').element().parentElement?.querySelector('.i-ri-arrow-right-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should forward custom classes to label separator item text and indicator', async () => {
const screen = await renderWithSafeViewport(
<Autocomplete open defaultValue="workflow" items={['workflow']}>

View File

@ -23,31 +23,6 @@ describe('Avatar', () => {
})
})
describe('Size variants', () => {
it.each([
{ size: 'xxs' as const, expectedClass: 'size-4' },
{ size: 'xs' as const, expectedClass: 'size-5' },
{ size: 'sm' as const, expectedClass: 'size-6' },
{ size: 'md' as const, expectedClass: 'size-8' },
{ size: 'lg' as const, expectedClass: 'size-9' },
{ size: 'xl' as const, expectedClass: 'size-10' },
{ size: '2xl' as const, expectedClass: 'size-12' },
{ size: '3xl' as const, expectedClass: 'size-16' },
])('should apply $expectedClass for size="$size"', async ({ size, expectedClass }) => {
const screen = await render(<Avatar name="Test" avatar={null} size={size} />)
const root = screen.container.firstElementChild as HTMLElement
expect(root).toHaveClass(expectedClass)
})
it('should default to md size when size is not specified', async () => {
const screen = await render(<Avatar name="Test" avatar={null} />)
const root = screen.container.firstElementChild as HTMLElement
expect(root).toHaveClass('size-8')
})
})
describe('className prop', () => {
it('should merge className with avatar variant classes on root', async () => {
const screen = await render(
@ -56,7 +31,6 @@ describe('Avatar', () => {
const root = screen.container.firstElementChild as HTMLElement
expect(root).toHaveClass('custom-class')
expect(root).toHaveClass('rounded-full', 'bg-primary-600')
})
})
@ -71,7 +45,7 @@ describe('Avatar', () => {
</AvatarRoot>,
)
await expect.element(screen.getByTestId('avatar-root')).toHaveClass('size-6')
await expect.element(screen.getByTestId('avatar-root')).toBeInTheDocument()
await expect.element(screen.getByText('J')).toBeInTheDocument()
await expect.element(screen.getByText('J')).toHaveStyle({ backgroundColor: 'rgb(1, 2, 3)' })
})
@ -81,9 +55,8 @@ describe('Avatar', () => {
it('should handle empty string name gracefully', async () => {
const screen = await render(<Avatar name="" avatar={null} />)
const fallback = screen.container.querySelector('.text-white') as HTMLElement
expect(fallback).toBeInTheDocument()
expect(fallback.textContent).toBe('')
expect(screen.container.firstElementChild).toBeInTheDocument()
expect(screen.container.textContent).toBe('')
})
it.each([

View File

@ -33,68 +33,6 @@ describe('Button', () => {
expect(button.tagName).toBe('A')
expect(button).toHaveAttribute('href', '/test')
})
it('applies base layout classes', async () => {
const screen = await render(<Button>Click me</Button>)
const btn = screen.getByRole('button').element()
expect(btn).toHaveClass('inline-flex', 'justify-center', 'items-center', 'cursor-pointer')
})
})
describe('variants', () => {
it('applies default secondary variant', async () => {
const screen = await render(<Button>Click me</Button>)
const btn = screen.getByRole('button').element()
expect(btn).toHaveClass('bg-components-button-secondary-bg', 'text-components-button-secondary-text')
})
it.each([
{ variant: 'primary' as const, expectedClass: 'bg-components-button-primary-bg' },
{ variant: 'secondary' as const, expectedClass: 'bg-components-button-secondary-bg' },
{ variant: 'secondary-accent' as const, expectedClass: 'text-components-button-secondary-accent-text' },
{ variant: 'ghost' as const, expectedClass: 'text-components-button-ghost-text' },
{ variant: 'ghost-accent' as const, expectedClass: 'hover:bg-state-accent-hover' },
{ variant: 'tertiary' as const, expectedClass: 'bg-components-button-tertiary-bg' },
])('applies $variant variant', async ({ variant, expectedClass }) => {
const screen = await render(<Button variant={variant}>Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass(expectedClass)
})
it('applies destructive tone with default variant', async () => {
const screen = await render(<Button tone="destructive">Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-secondary-bg')
})
it('applies destructive tone with primary variant', async () => {
const screen = await render(<Button variant="primary" tone="destructive">Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-primary-bg')
})
it('applies destructive tone with tertiary variant', async () => {
const screen = await render(<Button variant="tertiary" tone="destructive">Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-tertiary-bg')
})
it('applies destructive tone with ghost variant', async () => {
const screen = await render(<Button variant="ghost" tone="destructive">Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass('text-components-button-destructive-ghost-text')
})
})
describe('sizes', () => {
it('applies default medium size', async () => {
const screen = await render(<Button>Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass('h-8', 'rounded-lg')
})
it.each([
{ size: 'small' as const, expectedClass: 'h-6' },
{ size: 'medium' as const, expectedClass: 'h-8' },
{ size: 'large' as const, expectedClass: 'h-9' },
])('applies $size size', async ({ size, expectedClass }) => {
const screen = await render(<Button size={size}>Click me</Button>)
await expect.element(screen.getByRole('button')).toHaveClass(expectedClass)
})
})
describe('loading', () => {
@ -208,7 +146,6 @@ describe('Button', () => {
const screen = await render(<Button className="custom-class">Click me</Button>)
const btn = screen.getByRole('button').element()
expect(btn).toHaveClass('custom-class')
expect(btn).toHaveClass('inline-flex')
})
})

View File

@ -17,17 +17,14 @@ describe('Checkbox', () => {
await expect.element(checkbox).toHaveAttribute('data-unchecked', '')
await expect.element(checkbox).not.toHaveAttribute('data-checked')
await expect.element(checkbox).not.toHaveAttribute('data-indeterminate')
await expect.element(checkbox).toHaveClass('focus-visible:ring-2', 'focus-visible:ring-state-accent-solid')
})
it('should expose checked data attributes and icon styling hooks', async () => {
it('should expose checked data attributes', async () => {
const screen = await render(<Checkbox checked aria-label="Accept terms" />)
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
await expect.element(checkbox).toHaveAttribute('aria-checked', 'true')
await expect.element(checkbox).toHaveAttribute('data-checked', '')
await expect.element(checkbox).toHaveClass('data-checked:bg-components-checkbox-bg')
expect(screen.container.querySelector('.i-ri-check-line')).toBeInTheDocument()
})
it('should expose mixed state when indeterminate', async () => {
@ -36,8 +33,6 @@ describe('Checkbox', () => {
await expect.element(checkbox).toHaveAttribute('aria-checked', 'mixed')
await expect.element(checkbox).toHaveAttribute('data-indeterminate', '')
expect(screen.container.querySelector('.i-ri-check-line')).not.toBeInTheDocument()
expect(screen.container.querySelector('span span.rounded-full.bg-current')).toBeInTheDocument()
})
it('should call onCheckedChange with the next checked value', async () => {
@ -75,7 +70,6 @@ describe('Checkbox', () => {
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
await expect.element(checkbox).toHaveAttribute('data-disabled', '')
await expect.element(checkbox).toHaveClass('data-disabled:cursor-not-allowed')
asHTMLElement(checkbox.element()).click()
@ -125,6 +119,6 @@ describe('CheckboxSkeleton', () => {
const screen = await render(<CheckboxSkeleton data-testid="checkbox-skeleton" />)
expect(screen.container.querySelector('[role="checkbox"]')).not.toBeInTheDocument()
await expect.element(screen.getByTestId('checkbox-skeleton')).toHaveClass('bg-text-quaternary', 'opacity-20')
await expect.element(screen.getByTestId('checkbox-skeleton')).toBeInTheDocument()
})
})

View File

@ -62,28 +62,4 @@ describe('Collapsible wrappers', () => {
await expect.element(screen.getByRole('button', { name: 'Styled trigger' })).toHaveAttribute('data-panel-open', '')
await expect.element(screen.getByText('Styled panel')).toBeInTheDocument()
})
it('applies Dify disclosure defaults without a pressed active style', async () => {
const screen = await render(
<CollapsibleRoot defaultOpen>
<CollapsibleTrigger>Styled trigger</CollapsibleTrigger>
<CollapsiblePanel>Styled panel</CollapsiblePanel>
</CollapsibleRoot>,
)
const trigger = screen.getByRole('button', { name: 'Styled trigger' }).element()
const panel = screen.getByText('Styled panel').element()
expect(trigger).toHaveClass(
'hover:not-data-disabled:bg-components-panel-on-panel-item-bg-hover',
'focus-visible:ring-2',
'focus-visible:ring-state-accent-solid',
'data-panel-open:text-text-primary',
)
expect(trigger.className).not.toContain('active:')
expect(panel).toHaveClass(
'h-(--collapsible-panel-height)',
'data-ending-style:h-0',
'data-starting-style:h-0',
)
})
})

View File

@ -103,78 +103,15 @@ const renderInputCombobox = ({
describe('Combobox wrappers', () => {
describe('Select-like trigger', () => {
it('should render label and apply medium trigger classes by default', async () => {
it('should render label and trigger with combobox semantics', async () => {
const screen = await renderSelectLikeCombobox()
await expect.element(screen.getByText('Resource type')).toHaveClass('system-sm-medium')
await expect.element(screen.getByRole('combobox', { name: 'Resource type' })).toHaveClass('rounded-lg')
await expect.element(screen.getByRole('combobox', { name: 'Resource type' })).toHaveClass('system-sm-regular')
})
it('should apply small and large trigger size variants', async () => {
const smallScreen = await renderSelectLikeCombobox({
children: (
<ComboboxTrigger aria-label="Small resource type" size="small">
<ComboboxValue placeholder="Select resource" />
</ComboboxTrigger>
),
})
await expect.element(smallScreen.getByRole('combobox', { name: 'Small resource type' })).toHaveClass('rounded-md')
await expect.element(smallScreen.getByRole('combobox', { name: 'Small resource type' })).toHaveClass('system-xs-regular')
const largeScreen = await renderSelectLikeCombobox({
children: (
<ComboboxTrigger aria-label="Large resource type" size="large">
<ComboboxValue placeholder="Select resource" />
</ComboboxTrigger>
),
})
await expect.element(largeScreen.getByRole('combobox', { name: 'Large resource type' })).toHaveClass('rounded-[10px]')
await expect.element(largeScreen.getByRole('combobox', { name: 'Large resource type' })).toHaveClass('system-md-regular')
})
it('should render default trigger icon and support hiding it', async () => {
const withIcon = await renderSelectLikeCombobox()
expect(withIcon.getByTestId('trigger').element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
const withoutIcon = await renderSelectLikeCombobox({
children: (
<ComboboxTrigger aria-label="Resource type without icon" icon={false}>
<ComboboxValue placeholder="Select resource" />
</ComboboxTrigger>
),
})
expect(withoutIcon.getByRole('combobox', { name: 'Resource type without icon' }).element().querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument()
await expect.element(screen.getByText('Resource type')).toBeInTheDocument()
await expect.element(screen.getByRole('combobox', { name: 'Resource type' })).toBeInTheDocument()
})
})
describe('Input group and controls', () => {
it('should apply medium input group and input classes by default', async () => {
const screen = await renderInputCombobox()
await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-lg')
await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('px-3')
await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('system-sm-regular')
})
it('should apply large input group and input classes when large size is provided', async () => {
const screen = await renderInputCombobox({
children: (
<ComboboxInputGroup size="large" data-testid="input-group">
<ComboboxInput size="large" aria-label="Search resources" />
</ComboboxInputGroup>
),
})
await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-[10px]')
await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('px-4')
await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('system-md-regular')
})
it('should set input defaults and forward passthrough props', async () => {
const screen = await renderInputCombobox({
children: (
@ -201,8 +138,6 @@ describe('Combobox wrappers', () => {
await expect.element(screen.getByRole('button', { name: 'Clear combobox' })).toHaveAttribute('type', 'button')
await expect.element(screen.getByRole('button', { name: 'Open combobox options' })).toHaveAttribute('type', 'button')
expect(screen.getByRole('button', { name: 'Clear combobox' }).element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
expect(screen.getByRole('button', { name: 'Open combobox options' }).element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should rely on aria-labelledby when provided instead of injecting fallback labels', async () => {
@ -226,15 +161,11 @@ describe('Combobox wrappers', () => {
})
describe('Content and options', () => {
it('should use default overlay placement and Dify popup classes', async () => {
it('should use default overlay placement', async () => {
const screen = await renderSelectLikeCombobox({ open: true })
await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-side', 'bottom')
await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-align', 'start')
await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveClass('z-50')
await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('rounded-xl')
await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('w-(--anchor-width)')
await expect.element(screen.getByRole('listbox', { name: 'combobox list' })).toHaveClass('scroll-py-1')
})
it('should apply custom placement side and passthrough popup props', async () => {
@ -270,18 +201,6 @@ describe('Combobox wrappers', () => {
expect(onPopupClick).toHaveBeenCalledTimes(1)
})
it('should render item text indicator status and empty wrappers with design classes', async () => {
const screen = await renderSelectLikeCombobox({ open: true })
expect(screen.getByRole('option', { name: 'Workflow' }).element().className).not.toContain('mx-1')
await expect.element(screen.getByTestId('list').getByText('Workflow')).toHaveClass('system-sm-medium')
await expect.element(screen.getByTestId('status')).toHaveClass('text-text-tertiary')
await expect.element(screen.getByTestId('empty')).toHaveClass('system-sm-regular')
await expect.element(screen.getByTestId('empty')).toHaveClass('empty:p-0')
expect(screen.getByTestId('empty').element().getBoundingClientRect().height).toBe(0)
expect(screen.getByTestId('list').getByText('Workflow').element().parentElement?.querySelector('.i-ri-check-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should forward custom classes to group label separator item text and indicator', async () => {
const screen = await renderWithSafeViewport(
<Combobox open defaultValue="workflow" items={['workflow']}>
@ -368,7 +287,6 @@ describe('Combobox wrappers', () => {
await expect.element(screen.getByTestId('chips')).toHaveClass('custom-chips')
await expect.element(screen.getByText('maya').element().parentElement!).toHaveClass('custom-chip')
await expect.element(screen.getByRole('button', { name: 'Remove selected item' })).toHaveAttribute('type', 'button')
expect(screen.getByTestId('remove-chip').element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should preserve chip remove aria-labelledby over fallback label', async () => {

View File

@ -23,7 +23,6 @@ describe('Dialog wrapper', () => {
await expect.element(screen.getByRole('dialog')).toHaveTextContent('Dialog Title')
await expect.element(screen.getByRole('dialog')).toHaveTextContent('Dialog Description')
await expect.element(document.body.querySelector('.bg-background-overlay') as HTMLElement).toHaveClass('absolute', 'inset-0', 'z-50')
})
})

View File

@ -49,7 +49,7 @@ describe('Drawer wrapper', () => {
expect(screen.container).not.toContainElement(dialog)
await expect.element(dialog).toHaveTextContent('Workspace controls')
await expect.element(screen.getByText('Configure the current workspace.')).toBeInTheDocument()
await expect.element(screen.getByTestId('drawer-backdrop')).toHaveClass('absolute', 'inset-0', 'z-50')
await expect.element(screen.getByTestId('drawer-backdrop')).toBeInTheDocument()
asHTMLElement(screen.getByRole('button', { name: 'Close drawer' }).element()).click()

View File

@ -1,4 +1,3 @@
import * as React from 'react'
import { render } from 'vitest-browser-react'
import { Checkbox } from '../../checkbox'
import { CheckboxGroup } from '../../checkbox-group'
@ -38,9 +37,6 @@ describe('Field primitives', () => {
expect(label.tagName).toBe('LABEL')
expect(label).toHaveAttribute('for', asHTMLElement(input.element()).id)
expect(asHTMLElement(input.element()).getAttribute('aria-describedby')?.split(' ')).toContain(description.id)
await expect.element(input).toHaveClass('rounded-lg', 'system-sm-regular')
await expect.element(screen.getByText('Email')).toHaveClass('py-1', 'system-sm-medium')
await expect.element(screen.getByText('Used for account notifications.')).toHaveClass('py-0.5', 'body-xs-regular')
asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
@ -48,7 +44,6 @@ describe('Field primitives', () => {
const error = asHTMLElement(screen.getByText('Email is required.').element())
await expect.element(screen.getByText('Email is required.')).toBeInTheDocument()
await expect.element(input).toHaveAttribute('aria-invalid', 'true')
await expect.element(input).toHaveClass('data-invalid:border-components-input-border-destructive')
expect(asHTMLElement(input.element()).getAttribute('aria-describedby')?.split(' ')).toEqual(
expect.arrayContaining([description.id, error.id]),
)
@ -92,28 +87,9 @@ describe('Field primitives', () => {
await expect.element(screen.getByRole('group', { name: 'Features' })).toBeInTheDocument()
await expect.element(screen.getByRole('checkbox', { name: 'Search' })).toHaveAttribute('aria-checked', 'true')
await expect.element(screen.getByText('Choose at least one feature.')).toHaveClass('text-text-destructive', 'body-xs-regular')
})
it('should apply design-system control sizes when requested', async () => {
const screen = await render(
<React.Fragment>
<FieldRoot name="name">
<FieldLabel>Name</FieldLabel>
<FieldControl size="large" />
</FieldRoot>
<FieldRoot name="alias">
<FieldLabel>Alias</FieldLabel>
<FieldControl size="small" />
</FieldRoot>
</React.Fragment>,
)
await expect.element(screen.getByRole('textbox', { name: 'Name' })).toHaveClass('rounded-[10px]', 'py-[7px]', 'system-md-regular')
await expect.element(screen.getByRole('textbox', { name: 'Alias' })).toHaveClass('rounded-md', 'py-[3px]', 'system-xs-regular')
})
it('should expose the design-system read-only state', async () => {
it('should expose the read-only state', async () => {
const screen = await render(
<FieldRoot name="token">
<FieldLabel>Token</FieldLabel>
@ -122,6 +98,5 @@ describe('Field primitives', () => {
)
await expect.element(screen.getByRole('textbox', { name: 'Token' })).toHaveAttribute('readonly')
await expect.element(screen.getByRole('textbox', { name: 'Token' })).toHaveClass('read-only:cursor-default', 'read-only:focus:border-transparent')
})
})

View File

@ -5,7 +5,7 @@ import {
} from '../index'
describe('Fieldset primitives', () => {
it('should apply reset design-system classes', async () => {
it('should forward className to the fieldset and legend', async () => {
const screen = await render(
<FieldsetRoot className="custom-root">
<FieldsetLegend className="custom-legend">Permissions</FieldsetLegend>
@ -15,7 +15,7 @@ describe('Fieldset primitives', () => {
const legend = screen.getByText('Permissions').element() as HTMLElement
const fieldset = legend.closest('fieldset') as HTMLElement
await expect.element(fieldset).toHaveClass('m-0', 'min-w-0', 'border-0', 'p-0', 'custom-root')
await expect.element(legend).toHaveClass('mb-1', 'py-1', 'system-sm-medium', 'text-text-secondary', 'custom-legend')
await expect.element(fieldset).toHaveClass('custom-root')
await expect.element(legend).toHaveClass('custom-legend')
})
})

View File

@ -73,48 +73,6 @@ describe('FileTree', () => {
await expect.element(selectedFile).toHaveAttribute('data-selected')
})
it('uses Figma-aligned row, indentation, icon, and selected label styles', async () => {
const screen = await render(<TestFileTree />)
await expect.element(screen.getByLabelText('Project files')).toHaveClass('gap-px', 'p-1')
await expect.element(screen.getByRole('button', { name: 'button.tsx' })).toHaveClass('h-6', 'rounded-md', 'ps-2', 'pe-1.5', 'data-[selected]:bg-state-base-active')
await expect.element(screen.getByText('button.tsx')).toHaveClass('group-data-[selected]/file-tree-row:system-sm-medium', 'group-data-[selected]/file-tree-row:text-text-primary')
await expect.element(screen.getByText('README.md')).toHaveAttribute('data-label', 'README.md')
await expect.element(screen.getByText('README.md')).toHaveClass('after:content-[attr(data-label)]')
expect(screen.container.querySelector('.before\\:bottom-\\[-1px\\]')).toBeInTheDocument()
expect(screen.container.querySelector('.i-ri-folder-open-line')).toBeInTheDocument()
})
it('uses Remix fill icons for each non-folder file type', async () => {
const iconTypes = [
['file', 'i-ri-file-3-fill'],
['markdown', 'i-ri-markdown-fill'],
['json', 'i-ri-braces-fill'],
['image', 'i-ri-file-image-fill'],
['code', 'i-ri-file-code-fill'],
['database', 'i-ri-database-2-fill'],
['text', 'i-ri-file-text-fill'],
['pdf', 'i-ri-file-pdf-2-fill'],
['table', 'i-ri-file-excel-fill'],
['archive', 'i-ri-file-zip-fill'],
] as const
const screen = await render(
<FileTreeRoot aria-label="Icon examples">
<FileTreeList>
{iconTypes.map(([type]) => (
<FileTreeFile key={type}>
<FileTreeIcon type={type} />
<FileTreeLabel>{type}</FileTreeLabel>
</FileTreeFile>
))}
</FileTreeList>
</FileTreeRoot>,
)
for (const [, iconClassName] of iconTypes)
expect(screen.container.querySelector(`.${iconClassName}`)).toBeInTheDocument()
})
it('collapses and expands folders with click and native button keyboard behavior', async () => {
const screen = await render(<TestFileTree />)
const src = screen.getByRole('button', { name: 'src' }).element() as HTMLElement
@ -158,10 +116,9 @@ describe('FileTree', () => {
expect(onPreview).not.toHaveBeenCalled()
await expect.element(screen.getByRole('button', { name: 'disabled.txt' })).toBeDisabled()
await expect.element(screen.getByRole('button', { name: 'disabled.txt' })).toHaveAttribute('data-disabled')
await expect.element(screen.getByRole('button', { name: 'disabled.txt' })).toHaveClass('data-disabled:cursor-not-allowed')
})
it('styles disabled folder triggers from the resolved collapsible state', async () => {
it('resolves disabled folder triggers through the collapsible state', async () => {
const onOpenChange = vi.fn()
const screen = await render(
<FileTreeRoot aria-label="Disabled folders">
@ -188,6 +145,5 @@ describe('FileTree', () => {
expect(onOpenChange).not.toHaveBeenCalled()
await expect.element(trigger).toHaveAttribute('aria-disabled', 'true')
await expect.element(trigger).toHaveAttribute('aria-expanded', 'true')
await expect.element(trigger).toHaveClass('aria-disabled:cursor-not-allowed')
})
})

View File

@ -1,13 +1,12 @@
import * as React from 'react'
import { render } from 'vitest-browser-react'
import { FieldControl, FieldError, FieldLabel, FieldRoot } from '../../field'
import { FieldError, FieldLabel, FieldRoot } from '../../field'
import { Form } from '../../form'
import { Input } from '../index'
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
describe('Input', () => {
it('should render a labelled Base UI input with design-system classes', async () => {
it('should render a labelled Base UI input with its value', async () => {
const screen = await render(
<label>
Workspace name
@ -18,28 +17,6 @@ describe('Input', () => {
const input = screen.getByRole('textbox', { name: 'Workspace name' })
await expect.element(input).toHaveValue('Dify')
await expect.element(input).toHaveClass('rounded-lg', 'py-[7px]', 'system-sm-regular')
})
it('should apply size variants shared with FieldControl', async () => {
const screen = await render(
<React.Fragment>
<label>
Small input
<Input size="small" />
</label>
<div>
Large field
<FieldRoot name="largeField">
<FieldLabel>Large field</FieldLabel>
<FieldControl size="large" />
</FieldRoot>
</div>
</React.Fragment>,
)
await expect.element(screen.getByRole('textbox', { name: 'Small input' })).toHaveClass('rounded-md', 'py-[3px]', 'system-xs-regular')
await expect.element(screen.getByRole('textbox', { name: 'Large field' })).toHaveClass('rounded-[10px]', 'py-[7px]', 'system-md-regular')
})
it('should use FieldRoot invalid state', async () => {
@ -54,7 +31,6 @@ describe('Input', () => {
await expect.element(input).toHaveAttribute('aria-invalid', 'true')
await expect.element(input).toHaveAttribute('data-invalid')
await expect.element(input).toHaveClass('data-invalid:border-components-input-border-destructive')
})
it('should integrate with FieldRoot and Base UI Form validation', async () => {

View File

@ -7,38 +7,19 @@ describe('Kbd', () => {
const key = screen.getByText('⌘').element()
expect(key.tagName).toBe('KBD')
await expect.element(screen.getByText('⌘')).toHaveClass(
'h-4',
'min-w-4',
'px-px',
'rounded-sm',
'bg-components-kbd-bg-gray',
'text-text-tertiary',
'system-kbd',
)
})
it('applies the white variant for elevated or inverse surfaces', async () => {
const screen = await render(<Kbd color="white"></Kbd>)
await expect.element(screen.getByText('↵')).toHaveClass(
'bg-components-kbd-bg-white',
'text-text-primary-on-surface',
)
})
it('marks disabled keycaps visually without adding widget semantics', async () => {
const screen = await render(<Kbd disabled></Kbd>)
await expect.element(screen.getByText('⌘')).toHaveAttribute('data-disabled')
await expect.element(screen.getByText('⌘')).toHaveClass('opacity-30')
await expect.element(screen.getByText('⌘')).not.toHaveAttribute('aria-disabled')
})
it('merges custom classes with the design-system recipe', async () => {
const screen = await render(<Kbd className="custom-key h-5">K</Kbd>)
await expect.element(screen.getByText('K')).toHaveClass('custom-key', 'h-5')
await expect.element(screen.getByText('K')).toHaveClass('custom-key')
})
})

View File

@ -52,46 +52,7 @@ describe('Meter compound primitives', () => {
expect(indicator.getAttribute('style')).toContain('width: 42%')
})
it('applies tone="error" to the indicator', async () => {
const screen = await render(
<MeterRoot value={95} aria-label="Quota">
<MeterTrack>
<MeterIndicator tone="error" data-testid="indicator" />
</MeterTrack>
</MeterRoot>,
)
const indicator = screen.getByTestId('indicator').element() as HTMLElement
expect(indicator.className).toContain('bg-components-progress-error-progress')
})
it('applies tone="warning" to the indicator', async () => {
const screen = await render(
<MeterRoot value={85} aria-label="Quota">
<MeterTrack>
<MeterIndicator tone="warning" data-testid="indicator" />
</MeterTrack>
</MeterRoot>,
)
const indicator = screen.getByTestId('indicator').element() as HTMLElement
expect(indicator.className).toContain('bg-components-progress-warning-progress')
})
it('defaults to the neutral tone when none is supplied', async () => {
const screen = await render(
<MeterRoot value={25} aria-label="Quota">
<MeterTrack>
<MeterIndicator data-testid="indicator" />
</MeterTrack>
</MeterRoot>,
)
const indicator = screen.getByTestId('indicator').element() as HTMLElement
expect(indicator.className).toContain('bg-components-progress-bar-progress-solid')
})
it('forwards className to MeterTrack alongside the themed base classes', async () => {
it('forwards className to MeterTrack', async () => {
const screen = await render(
<MeterRoot value={10} aria-label="Quota">
<MeterTrack className="custom-track" data-testid="track">
@ -102,7 +63,6 @@ describe('Meter compound primitives', () => {
const track = screen.getByTestId('track').element() as HTMLElement
expect(track.className).toContain('custom-track')
expect(track.className).toContain('bg-components-progress-bar-bg')
})
it('renders MeterLabel and MeterValue inside a compound layout', async () => {

View File

@ -67,33 +67,16 @@ const renderNumberField = ({
describe('NumberField wrapper', () => {
describe('Group and input', () => {
it('should apply medium group classes by default and merge custom className', async () => {
it('should merge custom className on the group', async () => {
const screen = await renderNumberField({
groupProps: {
className: 'custom-group',
},
})
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-lg')
await expect.element(screen.getByTestId('group')).toHaveClass('focus-within:border-components-input-border-active')
await expect.element(screen.getByTestId('group')).toHaveClass('custom-group')
})
it('should apply large group and input classes when large size is provided', async () => {
const screen = await renderNumberField({
groupProps: {
size: 'large',
},
inputProps: {
size: 'large',
},
})
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-[10px]')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('px-4')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('py-2')
})
it('should surface field invalid state on the visual group', async () => {
const screen = await render(
<FieldRoot name="amount" invalid>
@ -107,7 +90,6 @@ describe('NumberField wrapper', () => {
)
await expect.element(screen.getByTestId('group')).toHaveAttribute('data-invalid')
await expect.element(screen.getByTestId('group')).toHaveClass('data-invalid:border-components-input-border-destructive')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveAttribute('aria-invalid', 'true')
})
@ -124,30 +106,22 @@ describe('NumberField wrapper', () => {
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveAttribute('autocorrect', 'off')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveAttribute('placeholder', 'Regular placeholder')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toBeRequired()
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('px-3')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('py-[7px]')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('system-sm-regular')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('custom-input')
})
})
describe('Unit and controls', () => {
it.each([
['medium', 'pe-2'],
['large', 'pe-2.5'],
] as const)('should apply the %s unit spacing variant', async (size, spacingClass) => {
it('should forward passthrough props to the unit', async () => {
const screen = await renderNumberField({
unitProps: {
size,
className: 'custom-unit',
title: `unit-${size}`,
title: 'unit-title',
},
})
await expect.element(screen.getByTestId('unit')).toHaveTextContent('ms')
await expect.element(screen.getByTestId('unit')).toHaveAttribute('title', `unit-${size}`)
await expect.element(screen.getByTestId('unit')).toHaveAttribute('title', 'unit-title')
await expect.element(screen.getByTestId('unit')).toHaveClass('custom-unit')
await expect.element(screen.getByTestId('unit')).toHaveClass(spacingClass)
})
it('should forward passthrough props to controls', async () => {
@ -169,8 +143,8 @@ describe('NumberField wrapper', () => {
controlsProps: {},
})
expect(screen.getByRole('button', { name: 'Increment value' }).element().querySelector('.i-ri-arrow-up-s-line')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Decrement value' }).element().querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Increment value' })).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Decrement value' })).toBeInTheDocument()
})
it('should preserve explicit aria labels and custom children', async () => {
@ -188,8 +162,6 @@ describe('NumberField wrapper', () => {
expect(screen.getByRole('button', { name: 'Increase amount' }).element()).toContainElement(screen.getByTestId('custom-increment-icon').element())
expect(screen.getByRole('button', { name: 'Decrease amount' }).element()).toContainElement(screen.getByTestId('custom-decrement-icon').element())
expect(screen.getByRole('button', { name: 'Increase amount' }).element().querySelector('.i-ri-arrow-up-s-line')).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Decrease amount' }).element().querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument()
})
it('should keep the fallback aria labels when aria-label is omitted in props', async () => {
@ -228,28 +200,21 @@ describe('NumberField wrapper', () => {
await expect.element(screen.getByRole('button', { name: 'Decrement from label' })).not.toHaveAttribute('aria-label')
})
it.each([
['medium', 'pt-1', 'pb-1'],
['large', 'pt-1.5', 'pb-1.5'],
] as const)('should apply the %s control button compound spacing classes', async (size, incrementClass, decrementClass) => {
it('should forward passthrough props to control buttons', async () => {
const screen = await renderNumberField({
controlsProps: {},
incrementProps: {
size,
className: 'custom-increment',
},
decrementProps: {
size,
className: 'custom-decrement',
title: `decrement-${size}`,
title: 'decrement-title',
},
})
await expect.element(screen.getByTestId('increment')).toHaveClass(incrementClass)
await expect.element(screen.getByTestId('increment')).toHaveClass('custom-increment')
await expect.element(screen.getByTestId('decrement')).toHaveClass(decrementClass)
await expect.element(screen.getByTestId('decrement')).toHaveClass('custom-decrement')
await expect.element(screen.getByTestId('decrement')).toHaveAttribute('title', `decrement-${size}`)
await expect.element(screen.getByTestId('decrement')).toHaveAttribute('title', 'decrement-title')
})
})
})

View File

@ -59,21 +59,14 @@ async function renderPagination({
}
describe('Pagination primitive', () => {
it('renders the Figma-aligned pagination structure with semantic navigation', async () => {
it('renders the pagination structure with semantic navigation', async () => {
const { screen } = await renderPagination()
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute('data-page', '2')
await expect.element(screen.getByTestId('content')).toHaveClass('grid', 'grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]')
await expect.element(screen.getByTestId('controls')).toHaveClass('justify-self-start', 'rounded-[10px]', 'bg-background-section-burn')
await expect.element(screen.getByRole('list')).toHaveClass('col-start-2', 'justify-self-center')
expect(screen.getByRole('group', { name: 'Items per page' }).element().parentElement).toHaveClass('col-start-3', 'justify-self-end')
await expect.element(screen.getByRole('button', { name: 'Previous page' })).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Next page' })).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toHaveTextContent('2/200')
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toHaveClass('h-7', 'px-2')
expect(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).not.toHaveClass('min-w-14')
await expect.element(screen.getByRole('button', { name: 'Page 2, current page' })).toHaveAttribute('aria-current', 'page')
await expect.element(screen.getByRole('button', { name: 'Page 2, current page' })).toHaveClass('bg-components-button-tertiary-bg')
await expect.element(screen.getByText('…')).toBeInTheDocument()
})
@ -117,7 +110,6 @@ describe('Pagination primitive', () => {
const input = asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()) as HTMLInputElement
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toHaveValue('2')
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toHaveClass('text-center', 'tabular-nums')
expect(input.parentElement?.parentElement?.parentElement).toHaveAttribute('data-page-summary', '2/200')
await vi.waitFor(() => {
expect(input.selectionStart).toBe(0)
@ -187,10 +179,7 @@ describe('Pagination primitive', () => {
it('uses segmented control semantics for page size', async () => {
const { screen, onPageSizeChange } = await renderPagination()
await expect.element(screen.getByRole('group', { name: 'Items per page' })).toHaveClass('bg-components-segmented-control-bg-normal')
await expect.element(screen.getByText('Items per page')).toHaveClass('opacity-0', 'group-hover/page-size:opacity-100', 'group-focus-within/page-size:opacity-100')
await expect.element(screen.getByRole('button', { name: '25' })).toHaveAttribute('aria-pressed', 'true')
await expect.element(screen.getByRole('button', { name: '25' })).toHaveClass('data-pressed:text-text-primary')
asHTMLElement(screen.getByRole('button', { name: '50' }).element()).click()
@ -241,8 +230,6 @@ describe('Pagination primitive', () => {
)
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
expect(screen.container.querySelector('nav[aria-label="Pagination"] > div')).toHaveClass('grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]')
await expect.element(screen.getByRole('list')).toHaveClass('col-start-2', 'justify-self-center')
})
it('does not expose invalid page controls when there are no pages', async () => {
@ -292,6 +279,5 @@ describe('Pagination primitive', () => {
const screen = await render(<PaginationSkeleton data-testid="skeleton" />)
await expect.element(screen.getByTestId('skeleton')).toHaveAttribute('aria-hidden', 'true')
await expect.element(screen.getByTestId('skeleton')).toHaveClass('select-none')
})
})

View File

@ -30,11 +30,6 @@ describe('PopoverContent', () => {
await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-side', 'bottom')
await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-align', 'center')
await expect.element(screen.getByRole('dialog', { name: 'default popover' })).toHaveTextContent('Default content')
await expect.element(screen.getByRole('dialog', { name: 'default popover' })).toHaveClass(
'outline-hidden',
'focus:outline-hidden',
'focus-visible:outline-hidden',
)
})
it('should apply parsed custom placement and custom offsets when placement props are provided', async () => {

View File

@ -37,29 +37,6 @@ describe('ProgressCircle', () => {
expect(screen.getByTestId('progress').element().querySelector('path')).toBeNull()
})
it('applies design kit size variants', async () => {
const screen = await render(<ProgressCircle value={50} size="large" aria-label="Uploading" data-testid="progress" />)
const root = screen.getByTestId('progress').element() as HTMLElement
const svg = root.querySelector('svg')!
expect(root.className).toContain('size-5')
expect(svg.getAttribute('width')).toBe('21')
expect(svg.getAttribute('height')).toBe('21')
})
it('applies color tokens to circle and sector', async () => {
const screen = await render(<ProgressCircle value={50} color="error" aria-label="Uploading" data-testid="progress" />)
const root = screen.getByTestId('progress').element() as HTMLElement
const circle = root.querySelector('circle')!
const path = root.querySelector('path')!
expect(circle.getAttribute('class')).toContain('fill-components-progress-error-bg')
expect(circle.getAttribute('class')).toContain('stroke-components-progress-error-border')
expect(path.getAttribute('class')).toContain('fill-components-progress-error-progress')
})
it('renders a deterministic progress sector', async () => {
const screen = await render(<ProgressCircle value={75} aria-label="Uploading" data-testid="progress" />)

View File

@ -69,7 +69,6 @@ describe('Radio', () => {
await expect.element(ssd).toHaveAttribute('aria-checked', 'true')
await expect.element(ssd).toHaveAttribute('data-checked', '')
await expect.element(ssd).toHaveClass('data-checked:border-components-radio-border-checked')
await expect.element(hdd).toHaveAttribute('aria-checked', 'false')
await expect.element(hdd).toHaveAttribute('data-unchecked', '')
})
@ -101,7 +100,6 @@ describe('Radio', () => {
const hdd = screen.getByRole('radio', { name: 'HDD' })
await expect.element(hdd).toHaveAttribute('data-disabled', '')
await expect.element(hdd).toHaveClass('data-disabled:cursor-not-allowed')
clickElement(hdd.element())
@ -170,9 +168,7 @@ describe('Radio', () => {
describe('RadioSkeleton', () => {
it('should render a visual placeholder without radio semantics', async () => {
const screen = await render(<RadioSkeleton />)
const skeleton = screen.container.querySelector<HTMLElement>('.rounded-full')
expect(screen.container.querySelector('[role="radio"]')).not.toBeInTheDocument()
await expect.element(skeleton).toHaveClass('rounded-full', 'opacity-20')
})
})

View File

@ -114,78 +114,20 @@ describe('scroll-area wrapper', () => {
})
describe('Scrollbar', () => {
it('should apply the default vertical scrollbar classes and orientation data attribute', async () => {
it('should apply the vertical orientation data attribute', async () => {
const screen = await renderScrollArea()
await expect.element(screen.getByTestId('scroll-area-vertical-scrollbar')).toHaveAttribute('data-orientation', 'vertical')
await expect.element(screen.getByTestId('scroll-area-vertical-scrollbar')).toHaveAttribute('data-dify-scrollbar')
await expect.element(screen.getByTestId('scroll-area-vertical-scrollbar')).toHaveClass(
'group/scrollbar',
'flex',
'overflow-clip',
'p-1',
'touch-none',
'select-none',
'opacity-100',
'transition-opacity',
'motion-reduce:transition-none',
'pointer-events-none',
'data-hovering:pointer-events-auto',
'data-scrolling:pointer-events-auto',
'data-[orientation=vertical]:absolute',
'data-[orientation=vertical]:inset-y-0',
'data-[orientation=vertical]:w-3',
'data-[orientation=vertical]:justify-center',
)
await expect.element(screen.getByTestId('scroll-area-vertical-thumb')).toHaveAttribute('data-orientation', 'vertical')
await expect.element(screen.getByTestId('scroll-area-vertical-thumb')).toHaveClass(
'shrink-0',
'rounded-sm',
'bg-state-base-handle',
'transition-[background-color]',
'motion-reduce:transition-none',
'data-[orientation=vertical]:w-1',
'group-data-hovering/scrollbar:bg-state-base-handle-hover',
'data-scrolling:bg-state-base-handle-hover',
'active:bg-state-base-handle-hover',
)
})
it('should apply horizontal scrollbar and thumb classes when orientation is horizontal', async () => {
it('should apply horizontal orientation data attributes', async () => {
const screen = await renderScrollArea()
await expect.element(screen.getByTestId('scroll-area-horizontal-scrollbar')).toHaveAttribute('data-orientation', 'horizontal')
await expect.element(screen.getByTestId('scroll-area-horizontal-scrollbar')).toHaveAttribute('data-dify-scrollbar')
await expect.element(screen.getByTestId('scroll-area-horizontal-scrollbar')).toHaveClass(
'group/scrollbar',
'flex',
'overflow-clip',
'p-1',
'touch-none',
'select-none',
'opacity-100',
'transition-opacity',
'motion-reduce:transition-none',
'pointer-events-none',
'data-hovering:pointer-events-auto',
'data-scrolling:pointer-events-auto',
'data-[orientation=horizontal]:absolute',
'data-[orientation=horizontal]:inset-x-0',
'data-[orientation=horizontal]:h-3',
'data-[orientation=horizontal]:items-center',
)
await expect.element(screen.getByTestId('scroll-area-horizontal-thumb')).toHaveAttribute('data-orientation', 'horizontal')
await expect.element(screen.getByTestId('scroll-area-horizontal-thumb')).toHaveClass(
'shrink-0',
'rounded-sm',
'bg-state-base-handle',
'transition-[background-color]',
'motion-reduce:transition-none',
'data-[orientation=horizontal]:h-1',
'group-data-hovering/scrollbar:bg-state-base-handle-hover',
'data-scrolling:bg-state-base-handle-hover',
'active:bg-state-base-handle-hover',
)
})
})
@ -195,16 +137,7 @@ describe('scroll-area wrapper', () => {
viewportClassName: 'custom-viewport-class',
})
await expect.element(screen.getByTestId('scroll-area-viewport')).toHaveClass(
'size-full',
'min-h-0',
'min-w-0',
'focus-visible:outline-2',
'focus-visible:-outline-offset-1',
'focus-visible:outline-solid',
'focus-visible:outline-state-accent-solid',
'custom-viewport-class',
)
await expect.element(screen.getByTestId('scroll-area-viewport')).toHaveClass('custom-viewport-class')
})
it('should let callers control scrollbar inset spacing via margin-based className overrides', async () => {
@ -265,7 +198,6 @@ describe('scroll-area wrapper', () => {
await vi.waitFor(() => {
expect(screen.getByTestId('scroll-area-corner').element()).toBeInTheDocument()
expect(screen.getByTestId('scroll-area-corner').element()).toHaveClass('bg-transparent')
})
}
finally {

View File

@ -113,7 +113,6 @@ describe('Select wrappers', () => {
)
await expect.element(screen.getByRole('combobox', { name: 'City' })).toBeInTheDocument()
await expect.element(screen.getByText('City')).toHaveClass('py-1', 'system-sm-medium', 'text-text-secondary')
})
it('should forward native trigger props when trigger props are provided', async () => {
@ -127,86 +126,20 @@ describe('Select wrappers', () => {
await expect.element(screen.getByRole('combobox', { name: 'Choose option' })).toBeDisabled()
})
it('should apply regular size variant classes by default', async () => {
const screen = await renderOpenSelect()
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toMatch(/system-sm-regular/)
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toMatch(/rounded-lg/)
})
it('should apply small size variant classes when size is small', async () => {
const screen = await renderOpenSelect({
triggerProps: { size: 'small' },
})
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toMatch(/system-xs-regular/)
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toMatch(/rounded-md/)
})
it('should apply large size variant classes when size is large', async () => {
const screen = await renderOpenSelect({
triggerProps: { size: 'large' },
})
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toMatch(/system-md-regular/)
})
it('should apply disabled styling via data attributes when disabled', async () => {
it('should expose disabled state via data attributes when disabled', async () => {
const screen = await renderOpenSelect({
triggerProps: { disabled: true },
})
await expect.element(screen.getByRole('combobox', { name: 'city select' })).toHaveAttribute('data-disabled')
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-disabled:bg-components-input-bg-disabled')
})
it('should apply disabled placeholder color class for compound state', async () => {
const screen = await renderOpenSelect({
triggerProps: { disabled: true },
})
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-disabled:data-placeholder:text-components-input-text-disabled')
})
it('should apply readonly styling via data attributes when Root is readOnly', async () => {
it('should expose readonly state via data attributes when Root is readOnly', async () => {
const screen = await renderOpenSelect({
rootProps: { readOnly: true },
})
await expect.element(screen.getByRole('combobox', { name: 'city select' })).toHaveAttribute('data-readonly')
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-readonly:bg-components-input-bg-normal')
})
it('should hide arrow icon via CSS when Root is readOnly', async () => {
const screen = await renderOpenSelect({
rootProps: { readOnly: true },
})
expect(screen.getByRole('combobox', { name: 'city select' }).element().querySelector('[class*="group-data-readonly:hidden"]')).toBeInTheDocument()
})
it('should set aria-hidden on decorative icons', async () => {
const screen = await renderOpenSelect()
expect(screen.getByRole('combobox', { name: 'city select' }).element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
})
it('should include placeholder color class via data attribute', async () => {
const screen = await renderOpenSelect()
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-placeholder:text-components-input-text-placeholder')
})
it('should render built-in chevron icon', async () => {
const screen = await renderOpenSelect()
expect(screen.getByRole('combobox', { name: 'city select' }).element().querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
})
it('should include open state feedback classes', async () => {
const screen = await renderOpenSelect()
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-popup-open:bg-state-base-hover-alt')
})
})

View File

@ -80,17 +80,6 @@ describe('Slider', () => {
expect(screen.container.querySelector('.outer-test')).toBeInTheDocument()
})
it('should expose focus-visible ring styles on the thumb wrapper', async () => {
const screen = await render(<Slider value={10} onValueChange={vi.fn()} aria-label="Value" />)
const thumb = screen.getByLabelText('Value').element().parentElement
expect(thumb).toHaveClass(
'has-[:focus-visible]:ring-2',
'has-[:focus-visible]:ring-state-accent-solid',
)
})
it('should not render prehydration script tags', async () => {
const screen = await render(<Slider value={10} onValueChange={vi.fn()} aria-label="Value" />)
@ -111,6 +100,6 @@ describe('Slider', () => {
)
await expect.element(screen.getByRole('slider', { name: 'Temperature' })).toHaveAttribute('aria-valuenow', '50')
await expect.element(screen.getByText('Temperature')).toHaveClass('py-1', 'system-sm-medium', 'text-text-secondary')
await expect.element(screen.getByText('Temperature')).toBeInTheDocument()
})
})

View File

@ -2,39 +2,10 @@ import { render } from 'vitest-browser-react'
import { StatusDot, StatusDotSkeleton } from '../index'
describe('StatusDot', () => {
it('renders a medium success dot by default', async () => {
it('is hidden from assistive tech by default', async () => {
const screen = await render(<StatusDot data-testid="dot" />)
const root = screen.getByTestId('dot').element() as HTMLElement
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
expect(root.className).toContain('size-2')
expect(root.className).toContain('bg-components-badge-status-light-success-bg')
expect(root.className).toContain('border-components-badge-status-light-success-border-inner')
expect(root.className).toContain('shadow-status-indicator-green-shadow')
})
it('uses small dot geometry', async () => {
const screen = await render(<StatusDot size="small" data-testid="dot" />)
const root = screen.getByTestId('dot').element() as HTMLElement
expect(root.className).toContain('size-1.5')
expect(root.className).toContain('rounded-xs')
})
it.each([
['warning', 'bg-components-badge-status-light-warning-bg', 'border-components-badge-status-light-warning-border-inner'],
['error', 'bg-components-badge-status-light-error-bg', 'border-components-badge-status-light-error-border-inner'],
['normal', 'bg-components-badge-status-light-normal-bg', 'border-components-badge-status-light-normal-border-inner'],
['disabled', 'bg-components-badge-status-light-disabled-bg', 'border-components-badge-status-light-disabled-border-inner'],
] as const)('applies %s status tokens', async (status, backgroundClass, borderClass) => {
const screen = await render(<StatusDot status={status} data-testid="dot" />)
const dot = screen.getByTestId('dot').element() as HTMLElement
expect(dot.className).toContain(backgroundClass)
expect(dot.className).toContain(borderClass)
})
it('keeps an explicit accessible label visible to assistive tech', async () => {
@ -44,14 +15,9 @@ describe('StatusDot', () => {
await expect.element(screen.getByTestId('dot')).not.toHaveAttribute('aria-hidden')
})
it('renders skeleton styling without status color', async () => {
it('renders the skeleton placeholder', async () => {
const screen = await render(<StatusDotSkeleton data-testid="dot" />)
const dot = screen.getByTestId('dot').element() as HTMLElement
expect(dot.className).toContain('bg-text-primary')
expect(dot.className).toContain('opacity-30')
expect(dot.className).not.toContain('bg-components-badge-status-light-success-bg')
expect(dot.className).not.toContain('border-components-badge-status-light-success-border-inner')
await expect.element(screen.getByTestId('dot')).toBeInTheDocument()
})
})

View File

@ -67,40 +67,22 @@ describe('Switch', () => {
const screen = await render(<Switch checked={false} disabled onCheckedChange={onCheckedChange} />)
const switchElement = screen.getByRole('switch')
await expect.element(switchElement).toHaveClass('data-disabled:cursor-not-allowed')
await expect.element(switchElement).toHaveAttribute('data-disabled', '')
asHTMLElement(switchElement.element()).click()
expect(onCheckedChange).not.toHaveBeenCalled()
})
it('should apply correct size classes', async () => {
const screen = await render(<Switch checked={false} size="xs" />)
await expect.element(screen.getByRole('switch')).toHaveClass('h-2.5', 'w-3.5', 'rounded-xs')
await screen.rerender(<Switch checked={false} size="sm" />)
await expect.element(screen.getByRole('switch')).toHaveClass('h-3', 'w-5')
await screen.rerender(<Switch checked={false} size="md" />)
await expect.element(screen.getByRole('switch')).toHaveClass('h-4', 'w-7')
await screen.rerender(<Switch checked={false} size="lg" />)
await expect.element(screen.getByRole('switch')).toHaveClass('h-5', 'w-9')
})
it('should apply custom className', async () => {
const screen = await render(<Switch checked={false} className="custom-test-class" />)
await expect.element(screen.getByRole('switch')).toHaveClass('custom-test-class')
})
it('should expose checked state styling hooks on the root and thumb', async () => {
it('should reflect checked state on the root and thumb', async () => {
const screen = await render(<Switch checked={false} />)
const switchElement = screen.getByRole('switch').element()
const thumb = getThumb(switchElement)
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked', 'data-checked:bg-components-toggle-bg')
expect(thumb).toHaveClass('data-checked:translate-x-[14px]')
expect(thumb).not.toHaveAttribute('data-checked')
await screen.rerender(<Switch checked={true} />)
@ -109,13 +91,9 @@ describe('Switch', () => {
expect(getThumb(screen.getByRole('switch').element())).toHaveAttribute('data-checked', '')
})
it('should expose disabled state styling hooks instead of relying on opacity', async () => {
it('should reflect disabled and checked states together', async () => {
const screen = await render(<Switch checked={false} disabled />)
await expect.element(screen.getByRole('switch')).toHaveClass(
'data-disabled:bg-components-toggle-bg-unchecked-disabled',
'data-disabled:data-checked:bg-components-toggle-bg-disabled',
)
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-disabled', '')
await screen.rerender(<Switch checked={true} disabled />)
@ -123,27 +101,12 @@ describe('Switch', () => {
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
})
it('should replace the native focus outline with the accent focus ring', async () => {
const screen = await render(<Switch checked={false} />)
await expect.element(screen.getByRole('switch')).toHaveClass(
'outline-hidden',
'focus-visible:ring-2',
'focus-visible:ring-state-accent-solid',
)
})
it('should respect prefers-reduced-motion', async () => {
const screen = await render(<Switch checked={false} />)
await expect.element(screen.getByRole('switch')).toHaveClass('motion-reduce:transition-none')
})
describe('loading state', () => {
it('should render as disabled when loading', async () => {
const onCheckedChange = vi.fn()
const screen = await render(<Switch checked={false} loading onCheckedChange={onCheckedChange} />)
const switchElement = screen.getByRole('switch')
await expect.element(switchElement).toHaveClass('data-disabled:cursor-not-allowed')
await expect.element(switchElement).toHaveAttribute('aria-busy', 'true')
await expect.element(switchElement).toHaveAttribute('data-disabled', '')
@ -159,24 +122,6 @@ describe('Switch', () => {
expect(screen.container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
})
it('should use checked data attributes to position spinner', async () => {
const screen = await render(<Switch checked={false} loading size="md" />)
const spinner = screen.container.querySelector('span[aria-hidden="true"]')
expect(spinner).toHaveClass(
'left-[calc(50%+6px)]',
'group-data-checked:left-[calc(50%-6px)]',
)
await screen.rerender(<Switch checked={true} loading size="md" />)
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
expect(screen.container.querySelector('span[aria-hidden="true"]')).toHaveClass(
'left-[calc(50%+6px)]',
'group-data-checked:left-[calc(50%-6px)]',
)
})
it('should not show spinner for xs and sm sizes', async () => {
const screen = await render(<Switch checked={false} loading size="xs" />)
expect(screen.container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
@ -204,19 +149,6 @@ describe('SwitchSkeleton', () => {
await expect.element(screen.getByTestId('skeleton-switch')).toBeInTheDocument()
})
it('should apply skeleton styles', async () => {
const screen = await render(<SwitchSkeleton data-testid="skeleton-switch" />)
await expect.element(screen.getByTestId('skeleton-switch')).toHaveClass('bg-text-quaternary', 'opacity-20')
})
it('should apply correct skeleton size classes', async () => {
const screen = await render(<SwitchSkeleton size="xs" data-testid="s" />)
await expect.element(screen.getByTestId('s')).toHaveClass('h-2.5', 'w-3.5', 'rounded-xs')
await screen.rerender(<SwitchSkeleton size="lg" data-testid="s" />)
await expect.element(screen.getByTestId('s')).toHaveClass('h-5', 'w-9', 'rounded-md')
})
it('should apply custom className to skeleton', async () => {
const screen = await render(<SwitchSkeleton className="custom-class" data-testid="s" />)
await expect.element(screen.getByTestId('s')).toHaveClass('custom-class')

View File

@ -27,30 +27,6 @@ describe('Tabs wrappers', () => {
await expect.element(screen.getByText('JS panel')).toBeInTheDocument()
})
it('keeps tabs styling minimal by default', async () => {
const screen = await render(
<Tabs defaultValue="first">
<TabsList>
<TabsTab value="first">First</TabsTab>
<TabsTab value="second">Second</TabsTab>
</TabsList>
</Tabs>,
)
await expect.element(screen.getByRole('tablist')).toHaveClass(
'flex',
'gap-4',
)
await expect.element(screen.getByRole('tab', { name: 'First' })).toHaveClass(
'touch-manipulation',
'focus-visible:outline-hidden',
'border-b-2',
'border-transparent',
'data-active:border-components-tab-active',
'data-active:text-text-primary',
)
})
it('calls onValueChange while leaving controlled value to the caller', async () => {
const onValueChange = vi.fn()
const screen = await render(

View File

@ -31,11 +31,10 @@ describe('Textarea', () => {
await expect.element(textarea).toHaveValue('A workspace for support automation.')
await expect.element(textarea).toHaveAccessibleDescription('Shown to workspace members.')
await expect.element(textarea).toHaveClass('min-h-20', 'overflow-auto', 'rounded-lg', 'system-sm-regular')
expect(asHTMLElement(textarea.element()).tagName).toBe('TEXTAREA')
})
it('should apply size variants and custom classes', async () => {
it('should apply custom classes', async () => {
const screen = await render(
<label>
Prompt
@ -43,13 +42,7 @@ describe('Textarea', () => {
</label>,
)
await expect.element(screen.getByRole('textbox', { name: 'Prompt' })).toHaveClass(
'rounded-[10px]',
'px-4',
'py-2',
'system-md-regular',
'resize-none',
)
await expect.element(screen.getByRole('textbox', { name: 'Prompt' })).toHaveClass('resize-none')
})
it('should call onValueChange and stay controlled until value changes', async () => {

View File

@ -37,8 +37,6 @@ describe('@langgenius/dify-ui/toast', () => {
await expect.element(screen.getByText('Your changes are available now.')).toBeInTheDocument()
const viewport = screen.getByRole('region', { name: 'Notifications' })
await expect.element(viewport).toHaveAttribute('aria-live', 'polite')
await expect.element(viewport).toHaveClass('z-60')
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
})
it('should keep multiple toast roots mounted in a collapsed stack', async () => {
@ -60,7 +58,6 @@ describe('@langgenius/dify-ui/toast', () => {
toast('Neutral toast')
await expect.element(screen.getByText('Neutral toast')).toBeInTheDocument()
expect(document.body.querySelector('[aria-hidden="true"].i-ri-information-2-fill')).not.toBeInTheDocument()
})
it('should wrap long unbroken toast content within the card width', async () => {
@ -220,7 +217,8 @@ describe('@langgenius/dify-ui/toast', () => {
await vi.advanceTimersByTimeAsync(1)
await vi.waitFor(() => {
expect(document.body).not.toHaveTextContent('Auto dismiss')
const toastDialog = document.body.querySelector('[role="dialog"]')
expect(toastDialog === null || toastDialog.hasAttribute('data-ending-style')).toBe(true)
})
})
@ -244,7 +242,6 @@ describe('@langgenius/dify-ui/toast', () => {
description: 'Preparing your data…',
})
await expect.element(screen.getByText('Loading')).toBeInTheDocument()
expect(document.body.querySelector('[aria-hidden="true"].i-ri-information-2-fill')).toBeInTheDocument()
toast.update(toastId, {
title: 'Done',
@ -255,7 +252,6 @@ describe('@langgenius/dify-ui/toast', () => {
await expect.element(screen.getByText('Done')).toBeInTheDocument()
await expect.element(screen.getByText('Your data is ready.')).toBeInTheDocument()
expect(document.body).not.toHaveTextContent('Loading')
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
})
it('should upsert an existing toast when add is called with the same id', async () => {
@ -322,6 +318,5 @@ describe('@langgenius/dify-ui/toast', () => {
await expect.element(screen.getByText('Saved')).toBeInTheDocument()
await expect.element(screen.getByText('Your changes are available now.')).toBeInTheDocument()
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
})
})