refactor(ui): decouple CSS dependencies and improve test quality (#35242)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-04-15 15:03:40 +08:00 committed by GitHub
parent 3bccdd6c9a
commit 50a55513d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 166 additions and 513 deletions

View File

@ -58,20 +58,4 @@ describe('AutomaticBtn', () => {
expect(mockOnClick).toHaveBeenCalledTimes(3)
})
})
describe('Styling', () => {
it('should have secondary-accent variant', () => {
render(<AutomaticBtn onClick={mockOnClick} />)
const button = screen.getByRole('button')
expect(button.className).toContain('secondary-accent')
})
it('should have small size', () => {
render(<AutomaticBtn onClick={mockOnClick} />)
const button = screen.getByRole('button')
expect(button.className).toContain('small')
})
})
})

View File

@ -95,12 +95,6 @@ describe('APIKeyInfoPanel - Cloud Edition', () => {
})
describe('Props and Styling', () => {
it('should render button with primary variant', () => {
scenarios.withAPIKeyNotSet()
const button = screen.getByRole('button')
expect(button).toHaveClass('btn-primary')
})
it('should render panel container with correct classes', () => {
const { container } = scenarios.withAPIKeyNotSet()
const panel = container.firstChild as HTMLElement

View File

@ -108,12 +108,6 @@ describe('APIKeyInfoPanel - Community Edition', () => {
})
describe('Props and Styling', () => {
it('should render button with primary variant', () => {
scenarios.withAPIKeyNotSet()
const button = screen.getByRole('button')
expect(button).toHaveClass('btn-primary')
})
it('should render panel container with correct classes', () => {
const { container } = scenarios.withAPIKeyNotSet()
const panel = container.firstChild as HTMLElement

View File

@ -1,7 +1,7 @@
import type { RenderOptions } from '@testing-library/react'
import type { Mock, MockedFunction } from 'vitest'
import type { ModalContextState } from '@/context/modal-context'
import { fireEvent, render } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import { noop } from 'es-toolkit/function'
import { defaultPlan } from '@/app/components/billing/config'
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
@ -81,6 +81,8 @@ type APIKeyInfoPanelRenderOptions = {
mockOverrides?: MockOverrides
} & Omit<RenderOptions, 'wrapper'>
const mainButtonName = /appOverview\.apiKeyInfo\.setAPIBtn/
// Setup function to configure mocks
function setupMocks(overrides: MockOverrides = {}) {
mockUseProviderContext.mockReturnValue({
@ -137,7 +139,7 @@ export const scenarios = {
export const assertions = {
// Should render main button
shouldRenderMainButton: () => {
const button = document.querySelector('button.btn-primary')
const button = screen.getByRole('button', { name: mainButtonName })
expect(button).toBeInTheDocument()
return button
},
@ -174,9 +176,8 @@ export const assertions = {
export const interactions = {
// Click the main button
clickMainButton: () => {
const button = document.querySelector('button.btn-primary')
if (button)
fireEvent.click(button)
const button = screen.getByRole('button', { name: mainButtonName })
fireEvent.click(button)
return button
},
@ -191,6 +192,7 @@ export const interactions = {
// Text content keys for assertions
export const textKeys = {
button: mainButtonName,
selfHost: {
titleRow1: /appOverview\.apiKeyInfo\.selfHost\.title\.row1/,
titleRow2: /appOverview\.apiKeyInfo\.selfHost\.title\.row2/,

View File

@ -84,49 +84,6 @@ describe('InlineDeleteConfirm', () => {
})
})
describe('Variant prop', () => {
it('should render with delete variant by default', () => {
const onConfirm = vi.fn()
const onCancel = vi.fn()
const { getByText } = render(
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).toContain('btn-destructive-primary')
})
it('should render without destructive class for warning variant', () => {
const onConfirm = vi.fn()
const onCancel = vi.fn()
const { getByText } = render(
<InlineDeleteConfirm
variant="warning"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
})
it('should render without destructive class for info variant', () => {
const onConfirm = vi.fn()
const onCancel = vi.fn()
const { getByText } = render(
<InlineDeleteConfirm
variant="info"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
})
})
describe('Custom className', () => {
it('should apply custom className to wrapper', () => {
const onConfirm = vi.fn()

View File

@ -22,7 +22,6 @@ describe('NotionConnector', () => {
})
expect(button).toBeInTheDocument()
expect(button).toHaveClass('btn', 'btn-primary')
})
it('should trigger the onSetting callback when the real button is clicked', async () => {

View File

@ -4,7 +4,6 @@ import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogClose,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
@ -70,14 +69,16 @@ describe('AlertDialog wrapper', () => {
})
describe('User Interactions', () => {
it('should open and close dialog when trigger and close are clicked', async () => {
it('should open and close dialog when trigger and cancel button are clicked', async () => {
render(
<AlertDialog>
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Action Required</AlertDialogTitle>
<AlertDialogDescription>Please confirm the action.</AlertDialogDescription>
<AlertDialogClose>Cancel</AlertDialogClose>
<AlertDialogActions>
<AlertDialogCancelButton>Cancel</AlertDialogCancelButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>,
)
@ -109,8 +110,7 @@ describe('AlertDialog wrapper', () => {
expect(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
const confirmButton = screen.getByRole('button', { name: 'Confirm' })
expect(confirmButton).toHaveClass('btn-primary')
expect(confirmButton).toHaveClass('btn-destructive-primary')
expect(confirmButton).toHaveClass('bg-components-button-destructive-primary-bg')
})
it('should keep dialog open after confirm click and close via cancel helper', async () => {

View File

@ -10,7 +10,6 @@ export const AlertDialog = BaseAlertDialog.Root
export const AlertDialogTrigger = BaseAlertDialog.Trigger
export const AlertDialogTitle = BaseAlertDialog.Title
export const AlertDialogDescription = BaseAlertDialog.Description
export const AlertDialogClose = BaseAlertDialog.Close
type AlertDialogContentProps = {
children: React.ReactNode

View File

@ -53,7 +53,7 @@ function AvatarImage({
}: AvatarImageProps) {
return (
<BaseAvatar.Image
className={cn('inset-0 absolute size-full object-cover', className)}
className={cn('absolute inset-0 size-full object-cover', className)}
{...props}
/>
)

View File

@ -1,8 +1,6 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import { Button } from '../index'
afterEach(cleanup)
describe('Button', () => {
describe('rendering', () => {
it('renders children text', () => {
@ -31,58 +29,79 @@ describe('Button', () => {
expect(link).toHaveTextContent('Link')
expect(link).toHaveAttribute('href', '/test')
})
it('applies base layout classes', () => {
render(<Button>Click me</Button>)
const btn = screen.getByRole('button')
expect(btn).toHaveClass('inline-flex', 'justify-center', 'items-center', 'cursor-pointer')
})
})
describe('variants', () => {
it('applies default secondary variant', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-secondary')
const btn = screen.getByRole('button')
expect(btn).toHaveClass('bg-components-button-secondary-bg', 'text-components-button-secondary-text')
})
it.each([
'primary',
'secondary',
'secondary-accent',
'ghost',
'ghost-accent',
'tertiary',
] as const)('applies %s variant', (variant) => {
{ 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', ({ variant, expectedClass }) => {
render(<Button variant={variant}>Click me</Button>)
expect(screen.getByRole('button').className).toContain(`btn-${variant}`)
expect(screen.getByRole('button')).toHaveClass(expectedClass)
})
it('applies destructive tone with default variant', () => {
render(<Button tone="destructive">Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-destructive-secondary')
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-secondary-bg')
})
it('applies destructive tone with primary variant', () => {
render(<Button variant="primary" tone="destructive">Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-destructive-primary')
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-primary-bg')
})
it('applies destructive tone with tertiary variant', () => {
render(<Button variant="tertiary" tone="destructive">Click me</Button>)
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-tertiary-bg')
})
it('applies destructive tone with ghost variant', () => {
render(<Button variant="ghost" tone="destructive">Click me</Button>)
expect(screen.getByRole('button')).toHaveClass('text-components-button-destructive-ghost-text')
})
})
describe('sizes', () => {
it('applies default medium size', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-medium')
expect(screen.getByRole('button')).toHaveClass('h-8', 'rounded-lg')
})
it.each(['small', 'medium', 'large'] as const)('applies %s size', (size) => {
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', ({ size, expectedClass }) => {
render(<Button size={size}>Click me</Button>)
expect(screen.getByRole('button').className).toContain(`btn-${size}`)
expect(screen.getByRole('button')).toHaveClass(expectedClass)
})
})
describe('loading', () => {
it('shows spinner when loading', () => {
render(<Button loading>Click me</Button>)
expect(screen.getByRole('button').querySelector('.animate-spin')).toBeInTheDocument()
expect(screen.getByRole('button').querySelector('[aria-hidden="true"]')).toBeInTheDocument()
})
it('hides spinner when not loading', () => {
render(<Button loading={false}>Click me</Button>)
expect(screen.getByRole('button').querySelector('.animate-spin')).not.toBeInTheDocument()
expect(screen.getByRole('button').querySelector('[aria-hidden="true"]')).not.toBeInTheDocument()
})
it('auto-disables when loading', () => {
@ -137,6 +156,15 @@ describe('Button', () => {
})
})
describe('className merging', () => {
it('merges custom className with variant classes', () => {
render(<Button className="custom-class">Click me</Button>)
const btn = screen.getByRole('button')
expect(btn).toHaveClass('custom-class')
expect(btn).toHaveClass('inline-flex')
})
})
describe('ref forwarding', () => {
it('forwards ref to the button element', () => {
let buttonRef: HTMLButtonElement | null = null

View File

@ -1,148 +0,0 @@
@utility btn {
@apply inline-flex justify-center items-center cursor-pointer whitespace-nowrap
outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid;
&:is(:disabled, [data-disabled]) {
@apply cursor-not-allowed;
}
}
@utility btn-small {
@apply px-2 h-6 rounded-md text-xs font-medium;
}
@utility btn-medium {
@apply px-3.5 h-8 rounded-lg text-[13px] leading-4 font-medium;
}
@utility btn-large {
@apply px-4 h-9 rounded-[10px] text-sm font-semibold;
}
@utility btn-primary {
@apply shadow
bg-components-button-primary-bg
border-components-button-primary-border
hover:bg-components-button-primary-bg-hover
hover:border-components-button-primary-border-hover
text-components-button-primary-text;
&:is(:disabled, [data-disabled]) {
@apply shadow-none
bg-components-button-primary-bg-disabled
border-components-button-primary-border-disabled
text-components-button-primary-text-disabled;
}
}
@utility btn-secondary {
@apply border-[0.5px]
shadow-xs
backdrop-blur-[5px]
bg-components-button-secondary-bg
border-components-button-secondary-border
hover:bg-components-button-secondary-bg-hover
hover:border-components-button-secondary-border-hover
text-components-button-secondary-text;
&:is(:disabled, [data-disabled]) {
@apply backdrop-blur-xs
bg-components-button-secondary-bg-disabled
border-components-button-secondary-border-disabled
text-components-button-secondary-text-disabled;
}
}
@utility btn-secondary-accent {
@apply border-[0.5px]
shadow-xs
bg-components-button-secondary-bg
border-components-button-secondary-border
hover:bg-components-button-secondary-bg-hover
hover:border-components-button-secondary-border-hover
text-components-button-secondary-accent-text;
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-secondary-bg-disabled
border-components-button-secondary-border-disabled
text-components-button-secondary-accent-text-disabled;
}
}
@utility btn-tertiary {
@apply bg-components-button-tertiary-bg
hover:bg-components-button-tertiary-bg-hover
text-components-button-tertiary-text;
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-tertiary-bg-disabled
text-components-button-tertiary-text-disabled;
}
}
@utility btn-ghost {
@apply hover:bg-components-button-ghost-bg-hover
text-components-button-ghost-text;
&:is(:disabled, [data-disabled]) {
@apply text-components-button-ghost-text-disabled;
}
}
@utility btn-ghost-accent {
@apply hover:bg-state-accent-hover
text-components-button-secondary-accent-text;
&:is(:disabled, [data-disabled]) {
@apply text-components-button-secondary-accent-text-disabled;
}
}
@utility btn-destructive-primary {
@apply bg-components-button-destructive-primary-bg
border-components-button-destructive-primary-border
hover:bg-components-button-destructive-primary-bg-hover
hover:border-components-button-destructive-primary-border-hover
text-components-button-destructive-primary-text;
&:is(:disabled, [data-disabled]) {
@apply shadow-none
bg-components-button-destructive-primary-bg-disabled
border-components-button-destructive-primary-border-disabled
text-components-button-destructive-primary-text-disabled;
}
}
@utility btn-destructive-secondary {
@apply bg-components-button-destructive-secondary-bg
border-components-button-destructive-secondary-border
hover:bg-components-button-destructive-secondary-bg-hover
hover:border-components-button-destructive-secondary-border-hover
text-components-button-destructive-secondary-text;
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-destructive-secondary-bg-disabled
border-components-button-destructive-secondary-border-disabled
text-components-button-destructive-secondary-text-disabled;
}
}
@utility btn-destructive-tertiary {
@apply bg-components-button-destructive-tertiary-bg
hover:bg-components-button-destructive-tertiary-bg-hover
text-components-button-destructive-tertiary-text;
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-destructive-tertiary-bg-disabled
text-components-button-destructive-tertiary-text-disabled;
}
}
@utility btn-destructive-ghost {
@apply hover:bg-components-button-destructive-ghost-bg-hover
text-components-button-destructive-ghost-text;
&:is(:disabled, [data-disabled]) {
@apply text-components-button-destructive-ghost-text-disabled;
}
}

View File

@ -5,21 +5,47 @@ import { cva } from 'class-variance-authority'
import { cn } from '@/utils/classnames'
const buttonVariants = cva(
'btn',
'inline-flex cursor-pointer items-center justify-center whitespace-nowrap outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid data-[disabled]:cursor-not-allowed',
{
variants: {
variant: {
'primary': 'btn-primary',
'secondary': 'btn-secondary',
'secondary-accent': 'btn-secondary-accent',
'ghost': 'btn-ghost',
'ghost-accent': 'btn-ghost-accent',
'tertiary': 'btn-tertiary',
'primary': [
'border-components-button-primary-border bg-components-button-primary-bg text-components-button-primary-text shadow',
'hover:border-components-button-primary-border-hover hover:bg-components-button-primary-bg-hover',
'data-[disabled]:border-components-button-primary-border-disabled data-[disabled]:bg-components-button-primary-bg-disabled data-[disabled]:text-components-button-primary-text-disabled data-[disabled]:shadow-none',
],
'secondary': [
'border-[0.5px] shadow-xs backdrop-blur-[5px]',
'border-components-button-secondary-border bg-components-button-secondary-bg text-components-button-secondary-text',
'hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
'data-[disabled]:border-components-button-secondary-border-disabled data-[disabled]:bg-components-button-secondary-bg-disabled data-[disabled]:text-components-button-secondary-text-disabled data-[disabled]:backdrop-blur-xs',
],
'secondary-accent': [
'border-[0.5px] shadow-xs',
'border-components-button-secondary-border bg-components-button-secondary-bg text-components-button-secondary-accent-text',
'hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
'data-[disabled]:border-components-button-secondary-border-disabled data-[disabled]:bg-components-button-secondary-bg-disabled data-[disabled]:text-components-button-secondary-accent-text-disabled',
],
'tertiary': [
'bg-components-button-tertiary-bg text-components-button-tertiary-text',
'hover:bg-components-button-tertiary-bg-hover',
'data-[disabled]:bg-components-button-tertiary-bg-disabled data-[disabled]:text-components-button-tertiary-text-disabled',
],
'ghost': [
'text-components-button-ghost-text',
'hover:bg-components-button-ghost-bg-hover',
'data-[disabled]:text-components-button-ghost-text-disabled',
],
'ghost-accent': [
'text-components-button-secondary-accent-text',
'hover:bg-state-accent-hover',
'data-[disabled]:text-components-button-secondary-accent-text-disabled',
],
},
size: {
small: 'btn-small',
medium: 'btn-medium',
large: 'btn-large',
small: 'h-6 rounded-md px-2 text-xs font-medium',
medium: 'h-8 rounded-lg px-3.5 text-[13px] leading-4 font-medium',
large: 'h-9 rounded-[10px] px-4 text-sm font-semibold',
},
tone: {
default: '',
@ -27,10 +53,42 @@ const buttonVariants = cva(
},
},
compoundVariants: [
{ variant: 'primary', tone: 'destructive', class: 'btn-destructive-primary' },
{ variant: 'secondary', tone: 'destructive', class: 'btn-destructive-secondary' },
{ variant: 'tertiary', tone: 'destructive', class: 'btn-destructive-tertiary' },
{ variant: 'ghost', tone: 'destructive', class: 'btn-destructive-ghost' },
{
variant: 'primary',
tone: 'destructive',
class: [
'border-components-button-destructive-primary-border bg-components-button-destructive-primary-bg text-components-button-destructive-primary-text',
'hover:border-components-button-destructive-primary-border-hover hover:bg-components-button-destructive-primary-bg-hover',
'data-[disabled]:border-components-button-destructive-primary-border-disabled data-[disabled]:bg-components-button-destructive-primary-bg-disabled data-[disabled]:text-components-button-destructive-primary-text-disabled data-[disabled]:shadow-none',
],
},
{
variant: 'secondary',
tone: 'destructive',
class: [
'border-components-button-destructive-secondary-border bg-components-button-destructive-secondary-bg text-components-button-destructive-secondary-text',
'hover:border-components-button-destructive-secondary-border-hover hover:bg-components-button-destructive-secondary-bg-hover',
'data-[disabled]:border-components-button-destructive-secondary-border-disabled data-[disabled]:bg-components-button-destructive-secondary-bg-disabled data-[disabled]:text-components-button-destructive-secondary-text-disabled',
],
},
{
variant: 'tertiary',
tone: 'destructive',
class: [
'bg-components-button-destructive-tertiary-bg text-components-button-destructive-tertiary-text',
'hover:bg-components-button-destructive-tertiary-bg-hover',
'data-[disabled]:bg-components-button-destructive-tertiary-bg-disabled data-[disabled]:text-components-button-destructive-tertiary-text-disabled',
],
},
{
variant: 'ghost',
tone: 'destructive',
class: [
'text-components-button-destructive-ghost-text',
'hover:bg-components-button-destructive-ghost-bg-hover',
'data-[disabled]:text-components-button-destructive-ghost-text-disabled',
],
},
],
defaultVariants: {
variant: 'secondary',

View File

@ -1,15 +1,11 @@
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import {
Dialog,
DialogClose,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
DialogTrigger,
} from '../index'
describe('Dialog wrapper', () => {
@ -86,15 +82,4 @@ describe('Dialog wrapper', () => {
expect(onClick).not.toHaveBeenCalled()
})
})
describe('Exports', () => {
it('should map dialog aliases to the matching base dialog primitives', () => {
expect(Dialog).toBe(BaseDialog.Root)
expect(DialogTrigger).toBe(BaseDialog.Trigger)
expect(DialogTitle).toBe(BaseDialog.Title)
expect(DialogDescription).toBe(BaseDialog.Description)
expect(DialogClose).toBe(BaseDialog.Close)
expect(DialogPortal).toBe(BaseDialog.Portal)
})
})
})

View File

@ -12,10 +12,10 @@ import * as React from 'react'
import { cn } from '@/utils/classnames'
export const Dialog = BaseDialog.Root
/** @public */
export const DialogTrigger = BaseDialog.Trigger
export const DialogTitle = BaseDialog.Title
export const DialogDescription = BaseDialog.Description
export const DialogClose = BaseDialog.Close
export const DialogPortal = BaseDialog.Portal
type DialogCloseButtonProps = Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Close>, 'children'>

View File

@ -1,7 +1,5 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import Link from '@/next/link'
import {
DropdownMenu,
DropdownMenuContent,
@ -14,21 +12,6 @@ import {
DropdownMenuTrigger,
} from '../index'
vi.mock('@/next/link', () => ({
default: ({
href,
children,
...props
}: {
href: string
children?: ReactNode
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
<a href={href} {...props}>
{children}
</a>
),
}))
describe('dropdown-menu wrapper', () => {
describe('DropdownMenuContent', () => {
it('should position content at bottom-end with default placement when props are omitted', () => {
@ -295,13 +278,13 @@ describe('dropdown-menu wrapper', () => {
expect(link).not.toHaveAttribute('closeOnClick')
})
it('should preserve link semantics when render prop uses a custom link component', () => {
it('should preserve link semantics when render prop uses a custom anchor element', () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLinkItem
render={<Link href="/account" />}
render={<a href="/account" />}
aria-label="account link"
>
Account settings

View File

@ -6,7 +6,6 @@ import type {
NumberFieldInputProps,
NumberFieldUnitProps,
} from '../index'
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
import { render, screen } from '@testing-library/react'
import {
NumberField,
@ -67,17 +66,6 @@ const renderNumberField = ({
}
describe('NumberField wrapper', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Export mapping should stay aligned with the Base UI primitive.
describe('Exports', () => {
it('should map NumberField to the matching base primitive root', () => {
expect(NumberField).toBe(BaseNumberField.Root)
})
})
// Group and input wrappers should preserve the design-system variants and DOM defaults.
describe('Group and input', () => {
it('should apply regular group classes by default and merge custom className', () => {

View File

@ -1,12 +1,8 @@
import { Popover as BasePopover } from '@base-ui/react/popover'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import {
Popover,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from '..'
@ -93,15 +89,3 @@ describe('PopoverContent', () => {
})
})
})
describe('Popover aliases', () => {
describe('Export mapping', () => {
it('should map aliases to the matching base popover primitives when wrapper exports are imported', () => {
expect(Popover).toBe(BasePopover.Root)
expect(PopoverTrigger).toBe(BasePopover.Trigger)
expect(PopoverClose).toBe(BasePopover.Close)
expect(PopoverTitle).toBe(BasePopover.Title)
expect(PopoverDescription).toBe(BasePopover.Description)
})
})
})

View File

@ -9,7 +9,9 @@ import { cn } from '@/utils/classnames'
export const Popover = BasePopover.Root
export const PopoverTrigger = BasePopover.Trigger
export const PopoverClose = BasePopover.Close
/** @public */
export const PopoverTitle = BasePopover.Title
/** @public */
export const PopoverDescription = BasePopover.Description
type PopoverContentProps = {

View File

@ -9,7 +9,6 @@ import {
ScrollAreaThumb,
ScrollAreaViewport,
} from '../index'
import styles from '../index.module.css'
const renderScrollArea = (options: {
rootClassName?: string
@ -106,7 +105,7 @@ describe('scroll-area wrapper', () => {
const thumb = screen.getByTestId('scroll-area-vertical-thumb')
expect(scrollbar).toHaveAttribute('data-orientation', 'vertical')
expect(scrollbar).toHaveClass(styles.scrollbar)
expect(scrollbar).toHaveAttribute('data-dify-scrollbar')
expect(scrollbar).toHaveClass(
'flex',
'overflow-clip',
@ -144,7 +143,7 @@ describe('scroll-area wrapper', () => {
const thumb = screen.getByTestId('scroll-area-horizontal-thumb')
expect(scrollbar).toHaveAttribute('data-orientation', 'horizontal')
expect(scrollbar).toHaveClass(styles.scrollbar)
expect(scrollbar).toHaveAttribute('data-dify-scrollbar')
expect(scrollbar).toHaveClass(
'flex',
'overflow-clip',

View File

@ -3,7 +3,7 @@
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
import * as React from 'react'
import { cn } from '@/utils/classnames'
import styles from './index.module.css'
import './scroll-area.css'
export const ScrollAreaRoot = BaseScrollArea.Root
type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root>
@ -25,7 +25,6 @@ type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
}
const scrollAreaScrollbarClassName = cn(
styles.scrollbar,
'flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
'pointer-events-none data-hovering:pointer-events-auto',
'data-scrolling:pointer-events-auto',
@ -68,6 +67,7 @@ export function ScrollAreaScrollbar({
}: ScrollAreaScrollbarProps) {
return (
<BaseScrollArea.Scrollbar
data-dify-scrollbar=""
className={cn(scrollAreaScrollbarClassName, className)}
{...props}
/>

View File

@ -1,5 +1,5 @@
.scrollbar::before,
.scrollbar::after {
[data-dify-scrollbar]::before,
[data-dify-scrollbar]::after {
content: '';
position: absolute;
z-index: 1;
@ -9,7 +9,7 @@
transition: opacity 150ms ease;
}
.scrollbar[data-orientation='vertical']::before {
[data-dify-scrollbar][data-orientation='vertical']::before {
left: 50%;
top: 4px;
width: 4px;
@ -18,7 +18,7 @@
background: linear-gradient(to bottom, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
}
.scrollbar[data-orientation='vertical']::after {
[data-dify-scrollbar][data-orientation='vertical']::after {
left: 50%;
bottom: 4px;
width: 4px;
@ -27,7 +27,7 @@
background: linear-gradient(to top, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
}
.scrollbar[data-orientation='horizontal']::before {
[data-dify-scrollbar][data-orientation='horizontal']::before {
top: 50%;
left: 4px;
width: 12px;
@ -36,7 +36,7 @@
background: linear-gradient(to right, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
}
.scrollbar[data-orientation='horizontal']::after {
[data-dify-scrollbar][data-orientation='horizontal']::after {
top: 50%;
right: 4px;
width: 12px;
@ -45,31 +45,31 @@
background: linear-gradient(to left, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
}
.scrollbar[data-orientation='vertical']:not([data-overflow-y-start])::before {
[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-start])::before {
opacity: 1;
}
.scrollbar[data-orientation='vertical']:not([data-overflow-y-end])::after {
[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-end])::after {
opacity: 1;
}
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-start])::before {
[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-start])::before {
opacity: 1;
}
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-end])::after {
[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-end])::after {
opacity: 1;
}
.scrollbar[data-hovering] > [data-orientation],
.scrollbar[data-scrolling] > [data-orientation],
.scrollbar > [data-orientation]:active {
[data-dify-scrollbar][data-hovering] > [data-orientation],
[data-dify-scrollbar][data-scrolling] > [data-orientation],
[data-dify-scrollbar] > [data-orientation]:active {
background-color: var(--scroll-area-thumb-bg-active, var(--color-state-base-handle-hover));
}
@media (prefers-reduced-motion: reduce) {
.scrollbar::before,
.scrollbar::after {
[data-dify-scrollbar]::before,
[data-dify-scrollbar]::after {
transition: none;
}
}

View File

@ -1,7 +1,6 @@
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../index'
import { Tooltip, TooltipContent, TooltipTrigger } from '../index'
describe('TooltipContent', () => {
describe('Placement and offsets', () => {
@ -105,11 +104,3 @@ describe('TooltipContent', () => {
})
})
})
describe('Tooltip aliases', () => {
it('should map alias exports to BaseTooltip components when wrapper exports are imported', () => {
expect(TooltipProvider).toBe(BaseTooltip.Provider)
expect(Tooltip).toBe(BaseTooltip.Root)
expect(TooltipTrigger).toBe(BaseTooltip.Trigger)
})
})

View File

@ -85,21 +85,6 @@ describe('Actions', () => {
})
})
// Button Variants Tests
describe('Button Variants', () => {
it('should have primary variant for choose button', () => {
render(<Actions {...defaultProps} />)
const chooseButton = screen.getByText(/operations\.choose/i).closest('button')
expect(chooseButton).toHaveClass('btn-primary')
})
it('should have secondary variant for details button', () => {
render(<Actions {...defaultProps} />)
const detailsButton = screen.getByText(/operations\.details/i).closest('button')
expect(detailsButton).toHaveClass('btn-secondary')
})
})
describe('Layout', () => {
it('should have absolute positioning', () => {
const { container } = render(<Actions {...defaultProps} />)

View File

@ -160,24 +160,6 @@ describe('CSVUploader', () => {
expect(mockUpdateFile).toHaveBeenCalled()
})
})
it('should call updateFile with undefined when remove is clicked', () => {
const mockUpdateFile = vi.fn()
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
const { container } = render(
<CSVUploader {...defaultProps} file={mockFile} updateFile={mockUpdateFile} />,
)
const deleteButton = container.querySelector('.cursor-pointer')
if (deleteButton)
fireEvent.click(deleteButton)
expect(mockUpdateFile).toHaveBeenCalledWith()
})
})
describe('Validation', () => {

View File

@ -187,10 +187,7 @@ describe('EditMetadataBatchModal', () => {
})
// Find the primary save button (not the one in SelectMetadataModal)
const saveButtons = screen.getAllByText(/save/i)
const modalSaveButton = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (modalSaveButton)
fireEvent.click(modalSaveButton)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onSave).toHaveBeenCalled()
})
@ -443,13 +440,10 @@ describe('EditMetadataBatchModal', () => {
})
// Find the primary save button
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn) {
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
}
const saveBtn = screen.getByRole('button', { name: 'common.operation.save' })
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
expect(onSave).toHaveBeenCalledTimes(3)
})
@ -462,10 +456,7 @@ describe('EditMetadataBatchModal', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onSave).toHaveBeenCalledWith(
expect.any(Array),
@ -486,10 +477,7 @@ describe('EditMetadataBatchModal', () => {
if (checkboxContainer)
fireEvent.click(checkboxContainer)
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => {
expect(onSave).toHaveBeenCalledWith(
@ -511,10 +499,7 @@ describe('EditMetadataBatchModal', () => {
// Remove an item
fireEvent.click(screen.getByTestId('remove-1'))
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onSave).toHaveBeenCalled()
// The first argument should not contain the deleted item (id '1')

View File

@ -284,12 +284,7 @@ describe('DatasetMetadataDrawer', () => {
fireEvent.change(inputs[0], { target: { value: 'renamed_field' } })
// Find and click save button
const saveBtns = screen.getAllByText(/save/i)
const primaryBtn = saveBtns.find(btn =>
btn.closest('button')?.classList.contains('btn-primary'),
)
if (primaryBtn)
fireEvent.click(primaryBtn)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => {
expect(onRename).toHaveBeenCalled()

View File

@ -139,18 +139,6 @@ describe('SecretKeyButton', () => {
const button = screen.getByRole('button')
expect(button.className).toContain('px-3')
})
it('should have small size', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button.className).toContain('btn-small')
})
it('should have ghost variant', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button.className).toContain('btn-ghost')
})
})
describe('icon styling', () => {

View File

@ -120,12 +120,6 @@ describe('SystemModel', () => {
expect(screen.getByRole('button', { name: /system model settings/i })).toBeDisabled()
})
it('should render the primary button variant when configuration is required', () => {
render(<SystemModel {...defaultProps} notConfigured />)
expect(screen.getByRole('button', { name: /system model settings/i })).toHaveClass('btn-primary')
})
it('should close dialog when cancel is clicked', async () => {
render(<SystemModel {...defaultProps} />)
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))

View File

@ -156,29 +156,6 @@ describe('AddApiKeyButton', () => {
expect(screen.getByRole('button')).toHaveTextContent('Custom API Key')
})
it('should apply button variant', () => {
const pluginPayload = createPluginPayload()
render(
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant="primary"
/>,
{ wrapper: createWrapper() },
)
expect(screen.getByRole('button').className).toContain('btn-primary')
})
it('should use secondary-accent variant by default', () => {
const pluginPayload = createPluginPayload()
render(<AddApiKeyButton pluginPayload={pluginPayload} />, { wrapper: createWrapper() })
// Verify the default button has secondary-accent variant class
expect(screen.getByRole('button').className).toContain('btn-secondary-accent')
})
})
describe('Props Testing', () => {
@ -372,25 +349,6 @@ describe('AddOAuthButton', () => {
expect(screen.getByText('plugin.auth.setupOAuth')).toBeInTheDocument()
})
it('should apply button variant to setup button', () => {
const pluginPayload = createPluginPayload()
mockGetPluginOAuthClientSchema.mockReturnValue({
schema: [],
is_oauth_custom_client_enabled: false,
is_system_oauth_params_exists: false,
})
render(
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant="secondary"
/>,
{ wrapper: createWrapper() },
)
expect(screen.getByRole('button').className).toContain('btn-secondary')
})
})
describe('Rendering - Configured State', () => {

View File

@ -88,23 +88,6 @@ describe('VersionMismatchModal', () => {
})
})
describe('button variants', () => {
it('should render cancel button with secondary variant', () => {
render(<VersionMismatchModal {...defaultProps} />)
const cancelBtn = screen.getByRole('button', { name: /app\.newApp\.Cancel/ })
expect(cancelBtn).toHaveClass('btn-secondary')
})
it('should render confirm button with primary destructive variant', () => {
render(<VersionMismatchModal {...defaultProps} />)
const confirmBtn = screen.getByRole('button', { name: /app\.newApp\.Confirm/ })
expect(confirmBtn).toHaveClass('btn-primary')
expect(confirmBtn).toHaveClass('btn-destructive-primary')
})
})
describe('edge cases', () => {
it('should handle undefined versions gracefully', () => {
render(<VersionMismatchModal {...defaultProps} versions={undefined} />)

View File

@ -157,16 +157,6 @@ describe('ConfirmModal', () => {
// Act & Assert - This will fail the test if user.click throws an unhandled error
await user.click(confirmButton)
})
it('should have correct button variants', () => {
// Arrange & Act
renderComponent()
// Assert
const confirmButton = screen.getByText('common.operation.confirm')
expect(confirmButton).toHaveClass('btn-primary')
expect(confirmButton).toHaveClass('btn-destructive-primary')
})
})
// Edge Cases (REQUIRED)

View File

@ -8,7 +8,6 @@
@import '../components/base/action-button/index.css';
@import '../components/base/badge/index.css';
@import '../components/base/ui/button/index.css';
@import '../components/base/modal/index.css' layer(base);
@import '../components/base/premium-badge/index.css';
@import '../components/base/segmented-control/index.css';

View File

@ -3309,11 +3309,6 @@
"count": 1
}
},
"app/components/base/ui/avatar/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/video-gallery/VideoPlayer.tsx": {
"react/set-state-in-effect": {
"count": 1