From 5af8f6af4d4a7d3dafd420f6c7073d6aec57ff53 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:49:03 +0800 Subject: [PATCH] test(dify-ui): focus component tests on behavior (#39030) --- .../src/alert-dialog/__tests__/index.spec.tsx | 104 +----------------- .../src/alert-dialog/index.stories.tsx | 3 + .../src/avatar/__tests__/index.spec.tsx | 67 +---------- .../src/button/__tests__/index.spec.tsx | 64 +---------- .../src/collapsible/__tests__/index.spec.tsx | 47 ++------ .../dify-ui/src/collapsible/index.stories.tsx | 17 +++ .../src/file-tree/__tests__/index.spec.tsx | 19 ---- .../src/input/__tests__/index.spec.tsx | 13 --- .../src/select/__tests__/index.spec.tsx | 53 --------- .../src/status-dot/__tests__/index.spec.tsx | 8 +- .../src/switch/__tests__/index.spec.tsx | 96 +--------------- .../dify-ui/src/tabs/__tests__/index.spec.tsx | 41 ------- packages/dify-ui/src/tabs/index.stories.tsx | 13 +++ 13 files changed, 56 insertions(+), 489 deletions(-) diff --git a/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx b/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx index 71ae1085ad4..df94da72e6d 100644 --- a/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx @@ -1,46 +1,7 @@ import { render } from 'vitest-browser-react' -import { - AlertDialog, - AlertDialogActions, - AlertDialogCancelButton, - AlertDialogConfirmButton, - AlertDialogContent, - AlertDialogDescription, - AlertDialogTitle, - AlertDialogTrigger, -} from '../index' +import { AlertDialog, AlertDialogActions, AlertDialogContent, AlertDialogTitle } from '../index' describe('AlertDialog wrapper', () => { - describe('Rendering', () => { - it('should render alert dialog content when dialog is open', async () => { - const screen = await render( - - - Confirm Delete - This action cannot be undone. - - , - ) - - await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Confirm Delete') - await expect - .element(screen.getByRole('alertdialog')) - .toHaveTextContent('This action cannot be undone.') - }) - - it('should not render content when dialog is closed', async () => { - const screen = await render( - - - Hidden Title - - , - ) - - expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument() - }) - }) - describe('Props', () => { it('should apply custom className to popup', async () => { const screen = await render( @@ -67,77 +28,18 @@ describe('AlertDialog wrapper', () => { }) }) - describe('User Interactions', () => { - it('should open and close dialog when trigger and cancel button are clicked', async () => { - const screen = await render( - - Open Dialog - - Action Required - Please confirm the action. - - Cancel - - - , - ) - - expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument() - - await screen.getByRole('button', { name: 'Open Dialog' }).click() - await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Action Required') - - await screen.getByRole('button', { name: 'Cancel' }).click() - await vi.waitFor(() => { - expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument() - }) - }) - }) - describe('Composition Helpers', () => { - it('should render actions wrapper and confirm button', async () => { + it('should forward className to the actions wrapper', async () => { const screen = await render( Action Required - - Confirm - + , ) 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 () => { - const onConfirm = vi.fn() - - const screen = await render( - - Open Dialog - - Action Required - - Cancel - Confirm - - - , - ) - - await screen.getByRole('button', { name: 'Open Dialog' }).click() - await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument() - - await screen.getByRole('button', { name: 'Confirm' }).click() - expect(onConfirm).toHaveBeenCalledTimes(1) - await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument() - - await screen.getByRole('button', { name: 'Cancel' }).click() - await vi.waitFor(() => { - expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument() - }) }) }) }) diff --git a/packages/dify-ui/src/alert-dialog/index.stories.tsx b/packages/dify-ui/src/alert-dialog/index.stories.tsx index 03975f880af..bc2cf398148 100644 --- a/packages/dify-ui/src/alert-dialog/index.stories.tsx +++ b/packages/dify-ui/src/alert-dialog/index.stories.tsx @@ -67,6 +67,9 @@ export const Default: Story = { await expect(dialog).toBeVisible() }) + await userEvent.click(body.getByRole('button', { name: 'Delete' })) + await expect(dialog).toBeVisible() + await userEvent.click(body.getByRole('button', { name: 'Cancel' })) await waitFor(async () => { await expect( diff --git a/packages/dify-ui/src/avatar/__tests__/index.spec.tsx b/packages/dify-ui/src/avatar/__tests__/index.spec.tsx index b92cc39c9a3..8231d196e5d 100644 --- a/packages/dify-ui/src/avatar/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/avatar/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render } from 'vitest-browser-react' -import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '..' +import { Avatar } from '..' function stubImageLoader() { const originalImage = window.Image @@ -27,26 +27,12 @@ function stubImageLoader() { describe('Avatar', () => { describe('Rendering', () => { - it('should keep the fallback visible when avatar URL is provided before image load', async () => { - const screen = await render( - , - ) - - await expect.element(screen.getByText('J')).toBeInTheDocument() - }) - it('should render fallback with uppercase initial when avatar is null', async () => { const screen = await render() expect(screen.container.querySelector('img')).not.toBeInTheDocument() await expect.element(screen.getByText('A')).toBeInTheDocument() }) - - it('should render the fallback when avatar is provided', async () => { - const screen = await render() - - await expect.element(screen.getByText('J')).toBeInTheDocument() - }) }) describe('className prop', () => { @@ -58,58 +44,13 @@ describe('Avatar', () => { }) }) - describe('Primitives', () => { - it('should support composed avatar usage through exported primitives', async () => { - const screen = await render( - - - - J - - , - ) - - 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)' }) - }) - }) - - describe('Edge Cases', () => { - it('should handle empty string name gracefully', async () => { - const screen = await render() - - expect(screen.container.firstElementChild).toBeInTheDocument() - expect(screen.container.textContent).toBe('') - }) - - it.each([ - { name: '中文名', expected: '中', label: 'Chinese characters' }, - { name: '123User', expected: '1', label: 'number' }, - ])( - 'should display first character when name starts with $label', - async ({ name, expected }) => { - const screen = await render() - - await expect.element(screen.getByText(expected)).toBeInTheDocument() - }, - ) - - it('should handle empty string avatar as falsy value', async () => { - const screen = await render() - - expect(screen.container.querySelector('img')).not.toBeInTheDocument() - await expect.element(screen.getByText('T')).toBeInTheDocument() - }) - }) - describe('onLoadingStatusChange', () => { - it('should forward image loading status changes', async () => { + it('should show fallback until the image loads and forward status changes', async () => { const { images, restore } = stubImageLoader() const onStatusChange = vi.fn() try { - await render( + const screen = await render( { />, ) + await expect.element(screen.getByText('J')).toBeVisible() await vi.waitFor(() => { expect(onStatusChange).toHaveBeenCalledWith('loading') }) @@ -126,6 +68,7 @@ describe('Avatar', () => { await vi.waitFor(() => { expect(onStatusChange).toHaveBeenCalledWith('loaded') }) + await expect.element(screen.getByRole('img', { name: 'John' })).toBeVisible() } finally { restore() } diff --git a/packages/dify-ui/src/button/__tests__/index.spec.tsx b/packages/dify-ui/src/button/__tests__/index.spec.tsx index b4803f08782..c932e94ecfa 100644 --- a/packages/dify-ui/src/button/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/button/__tests__/index.spec.tsx @@ -7,16 +7,6 @@ const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElem describe('Button', () => { describe('rendering', () => { - it('renders children text', async () => { - const screen = await render() - await expect.element(screen.getByRole('button')).toHaveTextContent('Click me') - }) - - it('renders as a native button element by default', async () => { - const screen = await render() - expect(screen.getByRole('button').element().tagName).toBe('BUTTON') - }) - it('defaults to type="button"', async () => { const screen = await render() await expect.element(screen.getByRole('button')).toHaveAttribute('type', 'button') @@ -39,51 +29,8 @@ describe('Button', () => { }) }) - describe('loading', () => { - it('shows spinner when loading', async () => { - const screen = await render() - expect( - screen.getByRole('button').element().querySelector('[aria-hidden="true"]'), - ).toBeInTheDocument() - }) - - it('hides spinner when not loading', async () => { - const screen = await render() - expect( - screen.getByRole('button').element().querySelector('[aria-hidden="true"]'), - ).not.toBeInTheDocument() - }) - - it('keeps loading buttons focusable by default', async () => { - const screen = await render() - const button = screen.getByRole('button').element() - - expect(button).not.toHaveAttribute('disabled') - expect((button as HTMLButtonElement).disabled).toBe(false) - expect(button).toHaveAttribute('aria-disabled', 'true') - - asHTMLElement(button).focus() - expect(button).toHaveFocus() - }) - - it('does not set aria-busy when loading', async () => { - const screen = await render() - await expect.element(screen.getByRole('button')).not.toHaveAttribute('aria-busy') - }) - - it('does not set aria-busy when not loading', async () => { - const screen = await render() - await expect.element(screen.getByRole('button')).not.toHaveAttribute('aria-busy') - }) - }) - - describe('disabled', () => { - it('disables button when disabled prop is set', async () => { - const screen = await render() - await expect.element(screen.getByRole('button')).toBeDisabled() - }) - - it('does not keep normal disabled buttons focusable by default', async () => { + describe('disabled states', () => { + it('uses native disabled semantics for regular disabled buttons', async () => { const screen = await render() const button = screen.getByRole('button').element() @@ -102,13 +49,6 @@ describe('Button', () => { }) describe('events', () => { - it('fires onClick when clicked', async () => { - const onClick = vi.fn() - const screen = await render() - await screen.getByRole('button').click() - expect(onClick).toHaveBeenCalledTimes(1) - }) - it('does not implicitly submit a form through a loading submit button', async () => { const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault()) const screen = await render( diff --git a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx index 6ee69322065..3f8522632f3 100644 --- a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx @@ -2,38 +2,6 @@ import { render } from 'vitest-browser-react' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '../index' describe('Collapsible wrappers', () => { - it('renders the Base UI anatomy with an accessible trigger', async () => { - const screen = await render( - - Recovery keys - Panel content - , - ) - - await expect.element(screen.getByTestId('collapsible-root')).toBeInTheDocument() - await expect - .element(screen.getByRole('button', { name: 'Recovery keys' })) - .toHaveAttribute('data-panel-open', '') - await expect.element(screen.getByText('Panel content')).toBeInTheDocument() - }) - - it('toggles open state through the trigger without caller-owned state', async () => { - const screen = await render( - - Toggle section - Hidden content - , - ) - const trigger = screen.getByRole('button', { name: 'Toggle section' }) - - await expect.element(trigger).not.toHaveAttribute('data-panel-open') - - await trigger.click() - - await expect.element(trigger).toHaveAttribute('data-panel-open', '') - await expect.element(screen.getByText('Hidden content')).toBeInTheDocument() - }) - it('forwards className to every compound part', async () => { const screen = await render( @@ -49,17 +17,18 @@ describe('Collapsible wrappers', () => { expect(screen.container.querySelector('.custom-root')).toBeInTheDocument() }) - it('passes Base UI panel props through to the panel', async () => { + it('keeps closed panel content mounted when requested', async () => { const screen = await render( - - Styled trigger - Styled panel + + Recovery options + Recovery codes , ) await expect - .element(screen.getByRole('button', { name: 'Styled trigger' })) - .toHaveAttribute('data-panel-open', '') - await expect.element(screen.getByText('Styled panel')).toBeInTheDocument() + .element(screen.getByRole('button', { name: 'Recovery options' })) + .toHaveAttribute('aria-expanded', 'false') + await expect.element(screen.getByText('Recovery codes')).toBeInTheDocument() + await expect.element(screen.getByText('Recovery codes')).not.toBeVisible() }) }) diff --git a/packages/dify-ui/src/collapsible/index.stories.tsx b/packages/dify-ui/src/collapsible/index.stories.tsx index d8943fde28f..e90303c31cd 100644 --- a/packages/dify-ui/src/collapsible/index.stories.tsx +++ b/packages/dify-ui/src/collapsible/index.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import * as React from 'react' +import { expect, waitFor } from 'storybook/test' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '.' import { cn } from '../cn' @@ -76,6 +77,22 @@ export const DefaultClosed: Story = { ), + play: async ({ canvas, userEvent }) => { + const trigger = canvas.getByRole('button', { name: 'Recovery keys' }) + + await expect(trigger).toHaveAttribute('aria-expanded', 'false') + await expect(canvas.queryByText('alien-bean-pasta')).not.toBeInTheDocument() + + await userEvent.click(trigger) + + await expect(trigger).toHaveAttribute('aria-expanded', 'true') + await expect(canvas.getByText('alien-bean-pasta')).toBeVisible() + + await userEvent.click(trigger) + await waitFor(async () => { + await expect(canvas.queryByText('alien-bean-pasta')).not.toBeInTheDocument() + }) + }, } export const DefaultOpen: Story = { diff --git a/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx b/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx index 2b0c9673ca6..85a4eddc5ef 100644 --- a/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx @@ -67,25 +67,6 @@ describe('FileTree', () => { await expect.element(selectedFile).toHaveAttribute('data-selected') }) - it('collapses and expands folders with click and native button keyboard behavior', async () => { - const screen = await render() - const src = screen.getByRole('button', { name: 'src' }) - - await src.click() - - await expect - .element(screen.getByRole('button', { name: 'src' })) - .toHaveAttribute('aria-expanded', 'false') - expect(screen.container.textContent).not.toContain('components') - - await src.click() - - await expect - .element(screen.getByRole('button', { name: 'src' })) - .toHaveAttribute('aria-expanded', 'true') - await expect.element(screen.getByRole('button', { name: 'components' })).toBeInTheDocument() - }) - it('activates file preview buttons without navigation semantics', async () => { const onPreview = vi.fn() const screen = await render() diff --git a/packages/dify-ui/src/input/__tests__/index.spec.tsx b/packages/dify-ui/src/input/__tests__/index.spec.tsx index 0a21e8555dc..0bcdf350bde 100644 --- a/packages/dify-ui/src/input/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/input/__tests__/index.spec.tsx @@ -4,19 +4,6 @@ import { Form } from '../../form' import { Input } from '../index' describe('Input', () => { - it('should render a labelled Base UI input with its value', async () => { - const screen = await render( - , - ) - - const input = screen.getByRole('textbox', { name: 'Workspace name' }) - - await expect.element(input).toHaveValue('Dify') - }) - it('should use Field invalid state', async () => { const screen = await render( diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx index a29e73e4a9f..2802b145061 100644 --- a/packages/dify-ui/src/select/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx @@ -13,7 +13,6 @@ import { SelectValue, } from '../index' -const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement const renderWithSafeViewport = (ui: React.ReactNode) => render(
{ui}
) @@ -258,58 +257,6 @@ describe('Select wrappers', () => { }) describe('SelectItem', () => { - it('should render options when children are provided', async () => { - const screen = await renderOpenSelect() - - await expect.element(screen.getByRole('option', { name: 'Seattle' })).toBeInTheDocument() - await expect.element(screen.getByRole('option', { name: 'New York' })).toBeInTheDocument() - }) - - it('should navigate items with arrow keys', async () => { - const screen = await render( - , - ) - - const trigger = asHTMLElement(screen.getByRole('combobox', { name: 'city select' }).element()) - - trigger.focus() - trigger.dispatchEvent( - new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }), - ) - await expect - .element(screen.getByRole('option', { name: 'Seattle' })) - .toHaveAttribute('data-highlighted') - - const highlightedItem = asHTMLElement( - screen.getByRole('option', { name: 'Seattle' }).element(), - ) - highlightedItem.dispatchEvent( - new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }), - ) - - await expect - .element(screen.getByRole('option', { name: 'New York' })) - .toHaveAttribute('data-highlighted') - }) - it('should expose disabled item semantics', async () => { const screen = await render(