mirror of
https://github.com/langgenius/dify.git
synced 2026-07-22 03:08:33 +08:00
test(dify-ui): remove low-value style assertions (#38418)
This commit is contained in:
parent
2e1ab194b7
commit
a0f8347513
@ -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 () => {
|
||||
|
||||
@ -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']}>
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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" />)
|
||||
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user