diff --git a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx index 024c3984bcf..e6dbec5c0f6 100644 --- a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx @@ -5,8 +5,6 @@ import { Autocomplete, AutocompleteClear, AutocompleteEmpty, - AutocompleteGroup, - AutocompleteGroupLabel, AutocompleteInput, AutocompleteInputGroup, AutocompleteItem, @@ -16,7 +14,6 @@ import { AutocompletePopup, AutocompletePortal, AutocompletePositioner, - AutocompleteSeparator, AutocompleteStatus, AutocompleteTrigger, useAutocompleteFilteredItems, @@ -94,13 +91,13 @@ describe('Autocomplete wrappers', () => { .not.toBe(restingBoxShadow) }) - it('should set input defaults and forward passthrough props', async () => { + it('should disable autocomplete and expose placeholder and required state', async () => { const screen = await renderAutocomplete({ children: ( @@ -117,9 +114,6 @@ describe('Autocomplete wrappers', () => { await expect .element(screen.getByRole('combobox', { name: 'Search suggestions' })) .toBeRequired() - await expect - .element(screen.getByRole('combobox', { name: 'Search suggestions' })) - .toHaveClass('custom-input') }) it('should not inject input-only attributes into a custom textarea', async () => { @@ -245,45 +239,6 @@ describe('Autocomplete wrappers', () => { expect(onPopupClick).toHaveBeenCalledTimes(1) }) - it('should forward custom classes to label separator item text and indicator', async () => { - const screen = await renderWithSafeViewport( - - - - - - - - - - - Resources - - - - Workflow - - - - - - - - , - ) - - await expect.element(screen.getByText('Resources')).toHaveClass('custom-label') - await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator') - await expect - .element(screen.getByRole('option', { name: 'Workflow' })) - .toHaveClass('custom-item') - await expect.element(screen.getByText('Workflow')).toHaveClass('custom-text') - await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator') - }) - it('should navigate function-rendered items with arrow keys', async () => { const screen = await renderWithSafeViewport( diff --git a/packages/dify-ui/src/avatar/__tests__/index.spec.tsx b/packages/dify-ui/src/avatar/__tests__/index.spec.tsx index c7af2a633cd..f959083425c 100644 --- a/packages/dify-ui/src/avatar/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/avatar/__tests__/index.spec.tsx @@ -47,15 +47,6 @@ describe('Avatar', () => { }) }) - describe('className prop', () => { - it('should merge className with avatar variant classes on root', async () => { - const screen = await render() - - const root = screen.container.firstElementChild as HTMLElement - expect(root).toHaveClass('custom-class') - }) - }) - describe('onLoadingStatusChange', () => { it('should show fallback until the image loads and forward status changes', async () => { const { images, restore } = stubImageLoader() diff --git a/packages/dify-ui/src/button/__tests__/index.spec.tsx b/packages/dify-ui/src/button/__tests__/index.spec.tsx index 4e871ad27c8..2f9a9d4eea9 100644 --- a/packages/dify-ui/src/button/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/button/__tests__/index.spec.tsx @@ -66,28 +66,4 @@ describe('Button', () => { expect(onSubmit).not.toHaveBeenCalled() }) }) - - describe('className merging', () => { - it('merges custom className with variant classes', async () => { - const screen = await render() - const btn = screen.getByRole('button').element() - expect(btn).toHaveClass('custom-class') - }) - }) - - describe('ref forwarding', () => { - it('forwards ref to the button element', async () => { - let buttonRef: HTMLButtonElement | null = null - await render( - , - ) - expect(buttonRef).toBeInstanceOf(HTMLButtonElement) - }) - }) }) diff --git a/packages/dify-ui/src/combobox/__tests__/index.spec.tsx b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx index 893ff6c9001..c8f4be4f351 100644 --- a/packages/dify-ui/src/combobox/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx @@ -10,7 +10,6 @@ import { ComboboxCollection, ComboboxEmpty, ComboboxGroup, - ComboboxGroupLabel, ComboboxInput, ComboboxInputGroup, ComboboxInputTrigger, @@ -22,7 +21,6 @@ import { ComboboxPopup, ComboboxPortal, ComboboxPositioner, - ComboboxSeparator, ComboboxStatus, ComboboxTrigger, ComboboxValue, @@ -206,13 +204,13 @@ describe('Combobox wrappers', () => { .not.toBe(restingBoxShadow) }) - it('should set input defaults and forward passthrough props', async () => { + it('should disable autocomplete and expose placeholder and required state', async () => { const screen = await renderInputCombobox({ children: ( @@ -229,9 +227,6 @@ describe('Combobox wrappers', () => { await expect .element(screen.getByRole('combobox', { name: 'Search resources' })) .toBeRequired() - await expect - .element(screen.getByRole('combobox', { name: 'Search resources' })) - .toHaveClass('custom-input') }) it('should not inject input-only attributes into a custom textarea', async () => { @@ -403,43 +398,6 @@ describe('Combobox wrappers', () => { expect(status.element().getBoundingClientRect().height).toBe(0) }) - it('should forward custom classes to group label separator item text and indicator', async () => { - const screen = await renderWithSafeViewport( - - - - - - - - - - - Resources - - - Workflow - - - - - - - - , - ) - - await expect.element(screen.getByText('Resources')).toHaveClass('custom-label') - await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator') - await expect - .element(screen.getByRole('option', { name: 'Workflow' })) - .toHaveClass('custom-item') - await expect - .element(screen.getByTestId('custom-list').getByText('Workflow')) - .toHaveClass('custom-text') - await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator') - }) - it('should navigate function-rendered items with arrow keys', async () => { const screen = await renderWithSafeViewport( @@ -525,16 +483,16 @@ describe('Combobox wrappers', () => { await expect.element(screen.getByText('No reviewers selected')).toBeInTheDocument() }) - it('should render chip wrappers and default remove button label', async () => { + it('should give chip remove buttons a default accessible name and non-submit type', async () => { const screen = await renderWithSafeViewport( - + > {(selectedValue) => ( {selectedValue?.map((item) => ( - + {item} @@ -548,10 +506,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') diff --git a/packages/dify-ui/src/date-time/internal/picker-parts.tsx b/packages/dify-ui/src/date-time/internal/picker-parts.tsx index 778f6368099..c9d93978a62 100644 --- a/packages/dify-ui/src/date-time/internal/picker-parts.tsx +++ b/packages/dify-ui/src/date-time/internal/picker-parts.tsx @@ -8,6 +8,7 @@ import { cn } from '../../cn' import { DirectionProvider, useDirection } from '../../direction-provider' import { formLabelClassName } from '../../form-control-shared' import { IconButton } from '../../icon-button' +import { resolveClassName } from '../../internals/resolve-class-name' import { Popover, PopoverContent, PopoverTrigger } from '../../popover' type PickerOpenChangeDetails = Omit< @@ -278,7 +279,7 @@ function PickerTrigger({ 'flex h-8 w-63 max-w-full items-center justify-between gap-0.5 rounded-lg bg-components-input-bg-normal ps-3 pe-2 text-start system-sm-regular text-components-input-text-filled', 'hover:bg-state-base-hover-alt focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-state-accent-solid data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-popup-open:bg-state-base-hover-alt', !field.serializedValue && 'text-text-tertiary in-data-[theme=dark]:text-text-secondary', - typeof className === 'function' ? className(state) : className, + resolveClassName(className, state), ) } > @@ -364,7 +365,7 @@ function PickerContent({ cn( 'forced-colors:[&_button:focus-visible]:outline-2 forced-colors:[&_button:focus-visible]:outline-[Highlight] forced-colors:[&_button:focus-visible]:outline-solid', 'flex w-63 max-w-(--available-width) flex-col overflow-hidden border-0 p-0 inset-ring-[0.5px] inset-ring-components-panel-border backdrop-blur-[5px]', - typeof className === 'function' ? className(state) : className, + resolveClassName(className, state), ) } initialFocus={() => diff --git a/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx b/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx index 56e01500056..2e4864a0d8b 100644 --- a/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx @@ -346,36 +346,6 @@ describe('dropdown-menu wrapper', () => { }) describe('DropdownMenuSeparator', () => { - it('should forward passthrough props and handlers when separator props are provided', async () => { - const handleMouseEnter = vi.fn() - - const screen = await render( - - Open - - - - , - ) - - screen - .getByRole('separator', { name: 'actions divider' }) - .element() - .dispatchEvent( - new MouseEvent('mouseover', { - bubbles: true, - }), - ) - await expect - .element(screen.getByRole('separator', { name: 'actions divider' })) - .toHaveAttribute('id', 'menu-separator') - expect(handleMouseEnter).toHaveBeenCalledTimes(1) - }) - it('should keep surrounding menu rows rendered when separator is placed between items', async () => { const screen = await render( diff --git a/packages/dify-ui/src/fieldset/__tests__/index.spec.tsx b/packages/dify-ui/src/fieldset/__tests__/index.spec.tsx deleted file mode 100644 index 0d50455b613..00000000000 --- a/packages/dify-ui/src/fieldset/__tests__/index.spec.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { render } from 'vitest-browser-react' -import { Fieldset, FieldsetLegend } from '../index' - -describe('Fieldset primitives', () => { - it('should forward className to the fieldset and legend', async () => { - const screen = await render( -
- Permissions -
, - ) - - const legend = screen.getByText('Permissions').element() as HTMLElement - const fieldset = legend.closest('fieldset') as HTMLElement - - await expect.element(fieldset).toHaveClass('custom-root') - await expect.element(legend).toHaveClass('custom-legend') - }) -}) diff --git a/packages/dify-ui/src/form/__tests__/index.spec.tsx b/packages/dify-ui/src/form/__tests__/index.spec.tsx index 277318fcf48..e3c1bafbeff 100644 --- a/packages/dify-ui/src/form/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/form/__tests__/index.spec.tsx @@ -4,21 +4,6 @@ import { Input } from '../../input' import { Form } from '../index' describe('Form primitive', () => { - it('should render a native named form and merge custom class names', async () => { - const screen = await render( -
- - Name - - -
, - ) - - await expect - .element(screen.getByRole('form', { name: 'profile form' })) - .toHaveClass('custom-form') - }) - it('should call onFormSubmit with submitted values', async () => { const onFormSubmit = vi.fn() const screen = await render( diff --git a/packages/dify-ui/src/kbd/__tests__/index.spec.tsx b/packages/dify-ui/src/kbd/__tests__/index.spec.tsx index 3b5083a83f3..ef0fa6d67a1 100644 --- a/packages/dify-ui/src/kbd/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/kbd/__tests__/index.spec.tsx @@ -15,12 +15,6 @@ describe('Kbd', () => { await expect.element(screen.getByText('⌘')).toHaveAttribute('data-disabled') await expect.element(screen.getByText('⌘')).not.toHaveAttribute('aria-disabled') }) - - it('merges custom classes with the design-system recipe', async () => { - const screen = await render(K) - - await expect.element(screen.getByText('K')).toHaveClass('custom-key') - }) }) describe('KbdGroup', () => { diff --git a/packages/dify-ui/src/number-field/__tests__/index.spec.tsx b/packages/dify-ui/src/number-field/__tests__/index.spec.tsx index 6fc8a6d3d6b..b6daeb0f41d 100644 --- a/packages/dify-ui/src/number-field/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/number-field/__tests__/index.spec.tsx @@ -1,10 +1,8 @@ import type { NumberFieldControlsProps, NumberFieldDecrementProps, - NumberFieldGroupProps, NumberFieldIncrementProps, NumberFieldInputProps, - NumberFieldUnitProps, } from '../index' import * as React from 'react' import { userEvent } from 'vite-plus/test/browser' @@ -18,14 +16,11 @@ import { NumberFieldGroup, NumberFieldIncrement, NumberFieldInput, - NumberFieldUnit, } from '../index' type RenderNumberFieldOptions = { defaultValue?: number - groupProps?: Partial inputProps?: Partial - unitProps?: Partial & { children?: React.ReactNode } controlsProps?: Partial incrementProps?: Partial decrementProps?: Partial @@ -33,24 +28,15 @@ type RenderNumberFieldOptions = { const renderNumberField = ({ defaultValue = 8, - groupProps, inputProps, - unitProps, controlsProps, incrementProps, decrementProps, }: RenderNumberFieldOptions = {}) => { - const { children: unitChildren = 'ms', ...restUnitProps } = unitProps ?? {} - return render( - + - {unitProps && ( - - {unitChildren} - - )} {(controlsProps || incrementProps || decrementProps) && ( @@ -78,16 +64,6 @@ describe('NumberField wrapper', () => { .not.toBe(restingBoxShadow) }) - it('should merge custom className on the group', async () => { - const screen = await renderNumberField({ - groupProps: { - className: 'custom-group', - }, - }) - - await expect.element(screen.getByTestId('group')).toHaveClass('custom-group') - }) - it('should surface field invalid state on the visual group', async () => { const screen = await render( @@ -106,10 +82,9 @@ describe('NumberField wrapper', () => { .toHaveAttribute('aria-invalid', 'true') }) - it('should set input defaults and forward passthrough props', async () => { + it('should disable autocomplete and expose placeholder and required state', async () => { const screen = await renderNumberField({ inputProps: { - className: 'custom-input', placeholder: 'Regular placeholder', required: true, }, @@ -125,38 +100,6 @@ describe('NumberField wrapper', () => { .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('custom-input') - }) - }) - - describe('Unit and controls', () => { - it('should forward passthrough props to the unit', async () => { - const screen = await renderNumberField({ - unitProps: { - className: 'custom-unit', - title: 'unit-title', - }, - }) - - await expect.element(screen.getByTestId('unit')).toHaveTextContent('ms') - await expect.element(screen.getByTestId('unit')).toHaveAttribute('title', 'unit-title') - await expect.element(screen.getByTestId('unit')).toHaveClass('custom-unit') - }) - - it('should forward passthrough props to controls', async () => { - const screen = await renderNumberField({ - controlsProps: { - className: 'custom-controls', - title: 'controls-title', - }, - }) - - await expect - .element(screen.getByTestId('controls')) - .toHaveAttribute('title', 'controls-title') - await expect.element(screen.getByTestId('controls')).toHaveClass('custom-controls') }) }) @@ -238,25 +181,6 @@ describe('NumberField wrapper', () => { .element(screen.getByRole('button', { name: 'Decrement from label' })) .not.toHaveAttribute('aria-label') }) - - it('should forward passthrough props to control buttons', async () => { - const screen = await renderNumberField({ - controlsProps: {}, - incrementProps: { - className: 'custom-increment', - }, - decrementProps: { - className: 'custom-decrement', - title: 'decrement-title', - }, - }) - - await expect.element(screen.getByTestId('increment')).toHaveClass('custom-increment') - await expect.element(screen.getByTestId('decrement')).toHaveClass('custom-decrement') - await expect - .element(screen.getByTestId('decrement')) - .toHaveAttribute('title', 'decrement-title') - }) }) }) diff --git a/packages/dify-ui/src/pagination/__tests__/index.spec.tsx b/packages/dify-ui/src/pagination/__tests__/index.spec.tsx index 7e92c757f9a..ae852ccee51 100644 --- a/packages/dify-ui/src/pagination/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/pagination/__tests__/index.spec.tsx @@ -344,9 +344,7 @@ describe('Pagination primitive', () => {
  1. - - Four - + Four
, @@ -354,9 +352,6 @@ describe('Pagination primitive', () => { await screen.getByRole('button', { name: 'Go to page 4' }).click() - await expect - .element(screen.getByRole('button', { name: 'Go to page 4' })) - .toHaveClass('custom-page') expect(onPageChange).toHaveBeenCalledWith(4) }) diff --git a/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx b/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx index 6d4246b43d5..d3b06fde06b 100644 --- a/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx @@ -184,21 +184,13 @@ describe('Radio', () => { it('should support custom items with a visual RadioControl', async () => { const screen = await render( - } - > + }> Card option - + , ) - await expect - .element(screen.getByRole('radio', { name: 'Card option' })) - .toHaveClass('custom-card') - expect(screen.container.querySelector('.custom-control')).toBeInTheDocument() await expect .element(screen.getByRole('radio', { name: 'Card option' })) .toHaveAttribute('data-checked', '') diff --git a/packages/dify-ui/src/scroll-area/__tests__/index.spec.tsx b/packages/dify-ui/src/scroll-area/__tests__/index.spec.tsx index 9bfa04cbed8..a4ff9e36958 100644 --- a/packages/dify-ui/src/scroll-area/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/scroll-area/__tests__/index.spec.tsx @@ -33,47 +33,26 @@ const stubElementMetric = ( const renderScrollArea = ( options: { - rootClassName?: string contentStyle?: React.CSSProperties - viewportClassName?: string viewportStyle?: React.CSSProperties - verticalScrollbarClassName?: string - horizontalScrollbarClassName?: string - verticalThumbClassName?: string - horizontalThumbClassName?: string } = {}, ) => { return render( - - + +
Scrollable content
- - + + - +
, ) @@ -110,14 +89,14 @@ describe('scroll area', () => { - +
Scrollable content
- +
@@ -125,14 +104,11 @@ describe('scroll area', () => { ) const viewport = screen.getByRole('region', { name: 'Installed apps' }) - const content = screen.getByText('Scrollable content').element().parentElement await expect .element(screen.getByTestId('scroll-area-root')) .not.toHaveAttribute('role', 'region') - await expect.element(viewport).toHaveClass('custom-viewport-class') await expect.element(viewport).toHaveAccessibleName('Installed apps') - expect(content).toHaveClass('custom-content-class') await expect.element(screen.getByText('Scrollable content')).toBeInTheDocument() }) @@ -220,32 +196,6 @@ describe('scroll area', () => { }) describe('Props', () => { - it('should forward className to the viewport', async () => { - const screen = await renderScrollArea({ - viewportClassName: '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 () => { - const screen = await renderScrollArea({ - verticalScrollbarClassName: - 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:-me-3', - horizontalScrollbarClassName: - 'data-[orientation=horizontal]:mx-2 data-[orientation=horizontal]:mb-2', - }) - - await expect - .element(screen.getByTestId('scroll-area-vertical-scrollbar')) - .toHaveClass('data-[orientation=vertical]:my-2', 'data-[orientation=vertical]:-me-3') - await expect - .element(screen.getByTestId('scroll-area-horizontal-scrollbar')) - .toHaveClass('data-[orientation=horizontal]:mx-2', 'data-[orientation=horizontal]:mb-2') - }) - it('should let vertical layouts override the content minimum width without important CSS', async () => { const screen = await renderScrollArea({ contentStyle: { minWidth: 0 } }) const content = screen.getByTestId('scroll-area-content').element() diff --git a/packages/dify-ui/src/slider/__tests__/types.tsx b/packages/dify-ui/src/slider/__tests__/types.tsx new file mode 100644 index 00000000000..3724645ebdb --- /dev/null +++ b/packages/dify-ui/src/slider/__tests__/types.tsx @@ -0,0 +1,35 @@ +import type { SliderValueProps } from '@langgenius/dify-ui/slider' +import { Slider, SliderValue } from '@langgenius/dify-ui/slider' +import { expectTypeOf } from 'vite-plus/test' + +function sliderTypeContracts() { + const single = ( + { + expectTypeOf(value).toEqualTypeOf() + }} + /> + ) + + const initialRange: readonly [number, number] = [25, 75] + const range = ( + { + expectTypeOf(value).toEqualTypeOf() + }} + /> + ) + + const renderValue: NonNullable = (formattedValues) => + formattedValues.join(' – ') + const output = {renderValue} + + // @ts-expect-error Slider only supports numeric values and ranges. + const invalid = + + return [single, range, output, invalid] +} + +void sliderTypeContracts diff --git a/packages/dify-ui/src/slider/index.stories.tsx b/packages/dify-ui/src/slider/index.stories.tsx index d27cb55966a..03d60f70694 100644 --- a/packages/dify-ui/src/slider/index.stories.tsx +++ b/packages/dify-ui/src/slider/index.stories.tsx @@ -1,7 +1,15 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import type { SliderProps } from '.' -import * as React from 'react' -import { Slider, SliderControl, SliderIndicator, SliderLabel, SliderThumb, SliderTrack } from '.' +import { expect } from 'storybook/test' +import { + Slider, + SliderControl, + SliderIndicator, + SliderLabel, + SliderThumb, + SliderTrack, + SliderValue, +} from '.' const meta = { title: 'Base/Form/Slider', @@ -43,21 +51,17 @@ function SliderDemo({ defaultValue: _defaultValue, ...args }: SliderProps) { - const [value, setValue] = React.useState(initialValue) - return ( -
- - Demo slider - - - - - - - -
{value}
-
+ + Demo slider + + + + + + + + ) } @@ -69,6 +73,16 @@ export const Default: Story = { max: 100, step: 1, }, + play: async ({ canvas, userEvent }) => { + const slider = canvas.getByRole('slider', { name: 'Demo slider' }) + const output = canvas.getByRole('status') + await expect(output).toHaveTextContent('50') + await expect(output).toHaveAttribute('for', slider.id) + await userEvent.tab() + await expect(slider).toHaveFocus() + await userEvent.keyboard('{ArrowRight}') + await expect(output).toHaveTextContent('51') + }, } export const Decimal: Story = { @@ -123,33 +137,30 @@ export const ComposedWithLabel: Story = { type PriceRange = readonly [number, number] function RangeSliderDemo() { - const [range, setRange] = React.useState([25, 75]) - return ( -
- - value={range} - onValueChange={setRange} - min={0} - max={100} - className="flex-col gap-1" - > - Price range - - - - - - - - -
- {range[0]} – {range[1]} -
-
+ defaultValue={[25, 75]} min={0} max={100} className="w-80 flex-col gap-1"> + Price range + + + + + + + + +
) } export const Range: Story = { render: () => , + play: async ({ canvas, userEvent }) => { + const slider = canvas.getByRole('slider', { name: 'Minimum price' }) + const output = canvas.getByRole('status') + await expect(output).toHaveTextContent('25 – 75') + await userEvent.tab() + await expect(slider).toHaveFocus() + await userEvent.keyboard('{ArrowRight}') + await expect(output).toHaveTextContent('26 – 75') + }, } diff --git a/packages/dify-ui/src/slider/index.tsx b/packages/dify-ui/src/slider/index.tsx index 2c9f3571e91..53730e26ee3 100644 --- a/packages/dify-ui/src/slider/index.tsx +++ b/packages/dify-ui/src/slider/index.tsx @@ -5,10 +5,10 @@ import { cn } from '../cn' import { formLabelClassName } from '../form-control-shared' import { resolveClassName } from '../internals/resolve-class-name' -type SliderValue = number | readonly number[] -type SliderProps = BaseSlider.Root.Props +type SliderRootValue = NonNullable +type SliderProps = BaseSlider.Root.Props -function Slider({ +function Slider({ className, ...props }: SliderProps) { @@ -37,6 +37,9 @@ function SliderLabel({ className, ...props }: SliderLabelProps) { ) } +type SliderValueProps = BaseSlider.Value.Props +const SliderValue = BaseSlider.Value + type SliderControlProps = BaseSlider.Control.Props function SliderControl({ className, ...props }: SliderControlProps) { @@ -109,7 +112,15 @@ function SliderThumb({ className, ...props }: SliderThumbProps) { ) } -export { Slider, SliderControl, SliderIndicator, SliderLabel, SliderThumb, SliderTrack } +export { + Slider, + SliderControl, + SliderIndicator, + SliderLabel, + SliderThumb, + SliderTrack, + SliderValue, +} export type { SliderControlProps, @@ -118,4 +129,5 @@ export type { SliderProps, SliderThumbProps, SliderTrackProps, + SliderValueProps, } diff --git a/packages/dify-ui/src/switch/__tests__/index.spec.tsx b/packages/dify-ui/src/switch/__tests__/index.spec.tsx index ba368a3bb4b..e7c1ac98a2b 100644 --- a/packages/dify-ui/src/switch/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/switch/__tests__/index.spec.tsx @@ -14,11 +14,6 @@ describe('Switch', () => { await expect.element(switchElement).toHaveAttribute('data-disabled', '') }) - it('should apply custom className', async () => { - const screen = await render() - await expect.element(screen.getByRole('switch')).toHaveClass('custom-test-class') - }) - it('should reflect checked state on the root and thumb', async () => { const screen = await render() const switchElement = screen.getByRole('switch').element() diff --git a/packages/dify-ui/src/tabs/__tests__/index.spec.tsx b/packages/dify-ui/src/tabs/__tests__/index.spec.tsx deleted file mode 100644 index da312ea450b..00000000000 --- a/packages/dify-ui/src/tabs/__tests__/index.spec.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render } from 'vitest-browser-react' -import { Tabs, TabsList, TabsPanel, TabsTab } from '../index' - -describe('Tabs wrappers', () => { - it('forwards className to composable parts', async () => { - const screen = await render( - - - - First - - - - Panel - - , - ) - - await expect.element(screen.getByRole('tablist')).toHaveClass('custom-list') - await expect.element(screen.getByRole('tab', { name: 'First' })).toHaveClass('custom-tab') - expect(screen.getByText('Panel').element()).toHaveClass('custom-panel') - }) -}) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx index 6e004a9c2f2..46a6706b7bd 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { toast } from '@/app/notifications' import ConfigParamModal from '../config-param-modal' @@ -46,18 +47,6 @@ vi.mock('@/config', () => ({ ANNOTATION_DEFAULT: { score_threshold: 0.9 }, })) -vi.mock('../score-slider', () => ({ - default: ({ value, onChange }: { value: number; onChange: (value: number) => void }) => ( - onChange(Number((e.target as HTMLInputElement).value))} - /> - ), -})) - const defaultAnnotationConfig = { id: 'test-id', enabled: false, @@ -182,7 +171,7 @@ describe('ConfigParamModal', () => { />, ) - expect(screen.getByRole('slider')).toHaveValue('90') + expect(screen.getByRole('slider')).toHaveValue('0.9') }) it('should render configConfirmBtn when isInit is false', () => { @@ -287,8 +276,8 @@ describe('ConfigParamModal', () => { const slider = screen.getByRole('slider') expect(slider).toHaveAttribute('min', '0') - expect(slider).toHaveAttribute('max', '100') - expect(slider).toHaveValue('90') + expect(slider).toHaveAttribute('max', '1') + expect(slider).toHaveValue('0.9') }) it('should update embedding model when model selector is used', () => { @@ -404,7 +393,7 @@ describe('ConfigParamModal', () => { />, ) - expect(screen.getByRole('slider')).toHaveValue('90') + expect(screen.getByRole('slider')).toHaveValue('0.9') }) it('should preserve zero score threshold instead of falling back to default', async () => { @@ -473,6 +462,7 @@ describe('ConfigParamModal', () => { }) it('should save updated score after slider changes', async () => { + const user = userEvent.setup() const onSave = vi.fn().mockResolvedValue(undefined) render( { />, ) - fireEvent.change(screen.getByRole('slider'), { target: { value: '96' } }) + const slider = screen.getByRole('slider') + await user.click(slider) + await user.keyboard('{ArrowRight}') + await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('0.91')) const buttons = screen.getAllByRole('button') const saveBtn = buttons.find((b) => b.textContent?.includes('initSetup')) @@ -493,7 +486,7 @@ describe('ConfigParamModal', () => { await waitFor(() => { expect(onSave).toHaveBeenCalledWith( expect.objectContaining({ embedding_provider_name: 'openai' }), - 0.96, + 0.91, ) }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index a70e54ab823..245a4ff93d7 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -99,11 +99,11 @@ const ConfigParamModal: FC = ({ > { setAnnotationConfig({ ...annotationConfig, - score_threshold: val / 100, + score_threshold: val, }) }} /> diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx index fe0dbb229f5..5e661414a5c 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx @@ -1,15 +1,44 @@ -import { render, screen } from '@testing-library/react' +import { act, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import ScoreSlider from '..' describe('ScoreSlider', () => { - it('should expose the business score instead of the internal percentage', () => { - render() + it('should display and update the score with two decimal places', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + const { rerender } = render() const slider = screen.getByRole('slider', { name: 'appDebug.feature.annotation.scoreThreshold.title', }) - expect(slider).toHaveAttribute('aria-valuenow', '90') + expect(slider).toHaveAttribute('aria-valuenow', '0.9') expect(slider).toHaveAttribute('aria-valuetext', '0.90') - expect(screen.getByText('0.90')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('0.90') + expect(screen.getByRole('status')).toHaveAttribute('for', slider.id) + act(() => slider.focus()) + await user.keyboard('{ArrowRight}') + expect(onChange).toHaveBeenCalledWith(0.91, expect.anything()) + rerender() + expect(screen.getByRole('status')).toHaveTextContent('0.91') + expect(slider).toHaveAttribute('aria-valuetext', '0.91') + }) + + it.each([ + ['{PageUp}', 0.6], + ['{PageDown}', 0.4], + ['{Shift>}{ArrowRight}{/Shift}', 0.6], + ['{Shift>}{ArrowLeft}{/Shift}', 0.4], + ])('should adjust the score by 0.10 with %s', async (key, expectedValue) => { + const user = userEvent.setup() + const onChange = vi.fn() + render() + + const slider = screen.getByRole('slider', { + name: 'appDebug.feature.annotation.scoreThreshold.title', + }) + act(() => slider.focus()) + await user.keyboard(key) + + expect(onChange).toHaveBeenCalledWith(expectedValue, expect.anything()) }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx index f170d00ed5f..ff2458311bf 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx @@ -7,6 +7,7 @@ import { SliderLabel, SliderThumb, SliderTrack, + SliderValue, } from '@langgenius/dify-ui/slider' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -24,7 +25,7 @@ const clamp = (value: number, min: number, max: number) => { } const SCORE_MIN = 0 -const SCORE_MAX = 100 +const SCORE_MAX = 1 const ScoreSlider: FC = ({ className, value, onChange }) => { const { t } = useTranslation(['appDebug']) @@ -32,37 +33,28 @@ const ScoreSlider: FC = ({ className, value, onChange }) => { return (
-
- - - {t(($) => $['feature.annotation.scoreThreshold.title'], { ns: 'appDebug' })} - - - - - (sliderValue / 100).toFixed(2)} - /> - - - -
- {(safeValue / 100).toFixed(2)} -
-
+ + + {t(($) => $['feature.annotation.scoreThreshold.title'], { ns: 'appDebug' })} + + + + + + + + + +
0.0
diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/on-minute-selector.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/on-minute-selector.spec.tsx index a0b50a5b991..112a1f127ca 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/on-minute-selector.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/on-minute-selector.spec.tsx @@ -7,14 +7,18 @@ describe('trigger-schedule/on-minute-selector', () => { const user = userEvent.setup() const onChange = vi.fn() - render() + const { rerender } = render() const slider = screen.getByRole('slider', { name: 'workflowIntegrations.nodes.triggerSchedule.onMinute', }) + expect(screen.getByRole('status')).toHaveTextContent('15') + expect(screen.getByRole('status')).toHaveAttribute('for', slider.id) slider.focus() await user.keyboard('{ArrowRight}') expect(onChange).toHaveBeenCalledWith(16, expect.objectContaining({ activeThumbIndex: 0 })) + rerender() + expect(screen.getByRole('status')).toHaveTextContent('16') }) }) diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx index 9761104bf2c..740346467be 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx @@ -5,6 +5,7 @@ import { SliderLabel, SliderThumb, SliderTrack, + SliderValue, } from '@langgenius/dify-ui/slider' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -30,9 +31,7 @@ const OnMinuteSelector = ({ value = 0, onChange }: OnMinuteSelectorProps) => { {t(($) => $['nodes.triggerSchedule.onMinute'], { ns: 'workflowIntegrations' })}
-
- {value} -
+
diff --git a/web/app/oauth-callback/__tests__/page.spec.tsx b/web/app/oauth-callback/__tests__/page.spec.tsx index 5a89f68855e..b42c8f9fdea 100644 --- a/web/app/oauth-callback/__tests__/page.spec.tsx +++ b/web/app/oauth-callback/__tests__/page.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { createReactI18nextMock } from '@/test/i18n-mock' vi.mock('react-i18next', () => diff --git a/web/hooks/__tests__/use-oauth.spec.tsx b/web/hooks/__tests__/use-oauth.spec.tsx index f3d6d0e0eed..99a9979887d 100644 --- a/web/hooks/__tests__/use-oauth.spec.tsx +++ b/web/hooks/__tests__/use-oauth.spec.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react' import { renderToString } from 'react-dom/server' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useOAuthCallback } from '../use-oauth' const originalOpener = window.opener