diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md
index ee230e58fc1..632ff9b4acc 100644
--- a/.agents/skills/frontend-testing/SKILL.md
+++ b/.agents/skills/frontend-testing/SKILL.md
@@ -1,11 +1,11 @@
---
name: frontend-testing
-description: Write, update, or review Dify frontend tests using Vitest and React Testing Library. Trigger for frontend specs, test coverage requests, regressions, testability, or testing strategy under web/.
+description: Write, update, or review Dify frontend tests using Vitest and Testing Library. Trigger for frontend specs, test coverage requests, regressions, testability, or testing strategy under web/ or packages/dify-ui/.
---
# Dify Frontend Testing
-Use this skill for Vitest and React Testing Library work under `web/`. Do not use it for Python tests or Cucumber/Playwright tests under `e2e/`.
+Use this skill for Vitest work under `web/` and `packages/dify-ui/`. Do not use it for Python tests or Cucumber/Playwright tests under `e2e/`.
## Required Source
@@ -14,6 +14,7 @@ Before writing, changing, or reviewing frontend tests, read `web/docs/test.md` c
## Workflow
1. Read the source, its behavior owner, nearby specs, and relevant public dependencies.
+1. Identify whether the contract belongs in `web/`, Dify UI Browser Mode, or a styled Storybook test.
1. Apply the canonical guide to decide whether a test is needed and choose its boundary.
1. For a behavior change or bug fix, write or identify the failing scenario first when practical.
1. Implement one coherent scenario at a time and run the focused spec before expanding scope.
@@ -22,10 +23,14 @@ Before writing, changing, or reviewing frontend tests, read `web/docs/test.md` c
When reviewing existing tests, recommend deleting low-value tests as readily as adding missing behavior coverage.
-Run focused tests from `web/`:
+Run focused tests from the owning workspace:
```bash
+# web/
vp test run path/to/spec-or-directory
+
+# packages/dify-ui/
+vp test run --project unit src/path/to/spec
```
-Run broader checks only after the focused behavior passes.
+For styled Dify UI behavior, run `vp test --project storybook --run`. Run broader checks only after the focused behavior passes.
diff --git a/packages/dify-ui/.storybook/storybook.css b/packages/dify-ui/.storybook/storybook.css
index d4a567d9cd9..615c37ba4d4 100644
--- a/packages/dify-ui/.storybook/storybook.css
+++ b/packages/dify-ui/.storybook/storybook.css
@@ -1,12 +1,11 @@
@import 'tailwindcss';
+@import '../src/styles/styles.css';
@plugin '../src/plugins/icons.ts';
@source '../src';
@source '../.storybook';
-@import '../src/styles/styles.css';
-
html {
color-scheme: light;
}
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 e841b55d0ae..71ae1085ad4 100644
--- a/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/alert-dialog/__tests__/index.spec.tsx
@@ -10,8 +10,6 @@ import {
AlertDialogTrigger,
} from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('AlertDialog wrapper', () => {
describe('Rendering', () => {
it('should render alert dialog content when dialog is open', async () => {
@@ -86,10 +84,10 @@ describe('AlertDialog wrapper', () => {
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
- asHTMLElement(screen.getByRole('button', { name: 'Open Dialog' }).element()).click()
+ await screen.getByRole('button', { name: 'Open Dialog' }).click()
await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Action Required')
- asHTMLElement(screen.getByRole('button', { name: 'Cancel' }).element()).click()
+ await screen.getByRole('button', { name: 'Cancel' }).click()
await vi.waitFor(() => {
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
})
@@ -129,14 +127,14 @@ describe('AlertDialog wrapper', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Open Dialog' }).element()).click()
+ await screen.getByRole('button', { name: 'Open Dialog' }).click()
await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument()
- asHTMLElement(screen.getByRole('button', { name: 'Confirm' }).element()).click()
+ await screen.getByRole('button', { name: 'Confirm' }).click()
expect(onConfirm).toHaveBeenCalledTimes(1)
await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument()
- asHTMLElement(screen.getByRole('button', { name: 'Cancel' }).element()).click()
+ 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/autocomplete/__tests__/index.spec.tsx b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx
index d84c0c2a23f..477eb7a8642 100644
--- a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx
@@ -200,7 +200,7 @@ describe('Autocomplete wrappers', () => {
,
)
- asHTMLElement(screen.getByRole('dialog', { name: 'autocomplete popup' }).element()).click()
+ await screen.getByRole('dialog', { name: 'autocomplete popup' }).click()
await expect
.element(screen.getByRole('group', { name: 'autocomplete positioner' }))
diff --git a/packages/dify-ui/src/button/__tests__/index.spec.tsx b/packages/dify-ui/src/button/__tests__/index.spec.tsx
index d5ddfb8a421..b4803f08782 100644
--- a/packages/dify-ui/src/button/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/button/__tests__/index.spec.tsx
@@ -109,43 +109,6 @@ describe('Button', () => {
expect(onClick).toHaveBeenCalledTimes(1)
})
- it('does not fire onClick when disabled', async () => {
- const onClick = vi.fn()
- const screen = await render(
- ,
- )
- asHTMLElement(screen.getByRole('button').element()).click()
- expect(onClick).not.toHaveBeenCalled()
- })
-
- it('does not fire onClick when loading', async () => {
- const onClick = vi.fn()
- const screen = await render(
- ,
- )
- asHTMLElement(screen.getByRole('button').element()).click()
- expect(onClick).not.toHaveBeenCalled()
- })
-
- it('does not submit a form when a loading submit button is clicked', async () => {
- const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault())
- const screen = await render(
- ,
- )
-
- asHTMLElement(screen.getByRole('button', { name: 'Submit' }).element()).click()
-
- expect(onSubmit).not.toHaveBeenCalled()
- })
-
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/checkbox-group/__tests__/index.spec.tsx b/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx
index 2cb4dff52a3..8896c8cce7a 100644
--- a/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx
@@ -5,8 +5,6 @@ import { Field, FieldItem, FieldLabel } from '../../field'
import { Fieldset, FieldsetLegend } from '../../fieldset'
import { CheckboxGroup } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('CheckboxGroup', () => {
it('should manage selected values and parent mixed state', async () => {
function PermissionsDemo() {
@@ -35,7 +33,7 @@ describe('CheckboxGroup', () => {
await expect.element(parent).toHaveAttribute('data-indeterminate', '')
await expect.element(write).toHaveAttribute('aria-checked', 'false')
- asHTMLElement(parent.element()).click()
+ await parent.click()
await vi.waitFor(async () => {
await expect.element(parent).toHaveAttribute('aria-checked', 'true')
@@ -68,7 +66,7 @@ describe('CheckboxGroup', () => {
const analytics = screen.getByRole('checkbox', { name: 'Analytics' })
await expect.element(analytics).toHaveAttribute('aria-checked', 'false')
- asHTMLElement(analytics.element()).click()
+ await analytics.click()
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange.mock.calls[0]?.[0]).toEqual(['search', 'analytics'])
diff --git a/packages/dify-ui/src/checkbox/__tests__/index.spec.tsx b/packages/dify-ui/src/checkbox/__tests__/index.spec.tsx
index f3e9d33972a..f96abb09b82 100644
--- a/packages/dify-ui/src/checkbox/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/checkbox/__tests__/index.spec.tsx
@@ -1,8 +1,6 @@
import { render } from 'vitest-browser-react'
import { Checkbox, CheckboxIndicator, CheckboxRoot, CheckboxSkeleton } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('Checkbox', () => {
it('should render an unchecked checkbox with Base UI semantics', async () => {
const screen = await render()
@@ -36,7 +34,7 @@ describe('Checkbox', () => {
,
)
- asHTMLElement(screen.getByRole('checkbox', { name: 'Accept terms' }).element()).click()
+ await screen.getByRole('checkbox', { name: 'Accept terms' }).click()
expect(onCheckedChange).toHaveBeenCalledTimes(1)
expect(onCheckedChange.mock.calls[0]?.[0]).toBe(true)
@@ -49,7 +47,7 @@ describe('Checkbox', () => {
)
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
- asHTMLElement(checkbox.element()).click()
+ await checkbox.click()
expect(onCheckedChange.mock.calls[0]?.[0]).toBe(true)
await expect.element(checkbox).toHaveAttribute('aria-checked', 'false')
@@ -62,22 +60,11 @@ describe('Checkbox', () => {
})
it('should ignore interaction when disabled', async () => {
- const onCheckedChange = vi.fn()
- const screen = await render(
- ,
- )
+ const screen = await render()
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
+ await expect.element(checkbox).toBeDisabled()
await expect.element(checkbox).toHaveAttribute('data-disabled', '')
-
- asHTMLElement(checkbox.element()).click()
-
- expect(onCheckedChange).not.toHaveBeenCalled()
})
it('should submit checked and unchecked form values through the hidden input', async () => {
diff --git a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx
index 6f8579cdbb3..6ee69322065 100644
--- a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx
@@ -1,8 +1,6 @@
import { render } from 'vitest-browser-react'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('Collapsible wrappers', () => {
it('renders the Base UI anatomy with an accessible trigger', async () => {
const screen = await render(
@@ -30,7 +28,7 @@ describe('Collapsible wrappers', () => {
await expect.element(trigger).not.toHaveAttribute('data-panel-open')
- asHTMLElement(trigger.element()).click()
+ await trigger.click()
await expect.element(trigger).toHaveAttribute('data-panel-open', '')
await expect.element(screen.getByText('Hidden content')).toBeInTheDocument()
diff --git a/packages/dify-ui/src/combobox/__tests__/index.spec.tsx b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx
index 56ab5c79645..f02cf863616 100644
--- a/packages/dify-ui/src/combobox/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx
@@ -218,7 +218,7 @@ describe('Combobox wrappers', () => {
,
)
- asHTMLElement(screen.getByRole('dialog', { name: 'combobox popup' }).element()).click()
+ await screen.getByRole('dialog', { name: 'combobox popup' }).click()
await expect
.element(screen.getByRole('group', { name: 'combobox positioner' }))
diff --git a/packages/dify-ui/src/dialog/__tests__/index.spec.tsx b/packages/dify-ui/src/dialog/__tests__/index.spec.tsx
index 7d954862436..46cc27eead5 100644
--- a/packages/dify-ui/src/dialog/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/dialog/__tests__/index.spec.tsx
@@ -9,8 +9,6 @@ import {
DialogTrigger,
} from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('Dialog wrapper', () => {
describe('Rendering', () => {
it('should render dialog content when dialog is open', async () => {
@@ -90,11 +88,10 @@ describe('Dialog wrapper', () => {
})
it('should forward close button props to base primitive', async () => {
- const onClick = vi.fn()
const screen = await render(
,
@@ -102,8 +99,6 @@ describe('Dialog wrapper', () => {
const closeButton = screen.getByTestId('close-button')
await expect.element(closeButton).toBeDisabled()
- asHTMLElement(closeButton.element()).click()
- expect(onClick).not.toHaveBeenCalled()
})
})
})
diff --git a/packages/dify-ui/src/drawer/__tests__/index.spec.tsx b/packages/dify-ui/src/drawer/__tests__/index.spec.tsx
index d402e288812..a363d15589d 100644
--- a/packages/dify-ui/src/drawer/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/drawer/__tests__/index.spec.tsx
@@ -38,7 +38,7 @@ describe('Drawer wrapper', () => {
expect(document.body.querySelector('[role="dialog"]')).not.toBeInTheDocument()
- asHTMLElement(screen.getByRole('button', { name: 'Open settings' }).element()).click()
+ await screen.getByRole('button', { name: 'Open settings' }).click()
await vi.waitFor(() => {
expect(document.body.querySelector('[role="dialog"]')).toBeInTheDocument()
@@ -51,7 +51,7 @@ describe('Drawer wrapper', () => {
await expect.element(screen.getByText('Configure the current workspace.')).toBeInTheDocument()
await expect.element(screen.getByTestId('drawer-backdrop')).toBeInTheDocument()
- asHTMLElement(screen.getByRole('button', { name: 'Close drawer' }).element()).click()
+ await screen.getByRole('button', { name: 'Close drawer' }).click()
await vi.waitFor(() => {
expect(document.body.querySelector('[role="dialog"]')).not.toBeInTheDocument()
diff --git a/packages/dify-ui/src/field/__tests__/index.spec.tsx b/packages/dify-ui/src/field/__tests__/index.spec.tsx
index 44778c25470..0401e7446ea 100644
--- a/packages/dify-ui/src/field/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/field/__tests__/index.spec.tsx
@@ -33,7 +33,7 @@ describe('Field primitives', () => {
description.id,
)
- asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
+ await screen.getByRole('button', { name: 'Save' }).click()
await vi.waitFor(async () => {
const error = asHTMLElement(screen.getByText('Email is required.').element())
@@ -58,7 +58,7 @@ describe('Field primitives', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
+ await screen.getByRole('button', { name: 'Save' }).click()
expect(onFormSubmit).toHaveBeenCalledTimes(1)
expect(onFormSubmit.mock.calls[0]?.[0]).toMatchObject({ apiKey: 'sk-test' })
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 c11ee693d88..2b0c9673ca6 100644
--- a/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/file-tree/__tests__/index.spec.tsx
@@ -10,8 +10,6 @@ import {
FileTreeList,
} from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
function TestFileTree({ onPreview = vi.fn() }: { onPreview?: (itemId: string) => void }) {
return (
@@ -71,16 +69,16 @@ describe('FileTree', () => {
it('collapses and expands folders with click and native button keyboard behavior', async () => {
const screen = await render()
- const src = screen.getByRole('button', { name: 'src' }).element() as HTMLElement
+ const src = screen.getByRole('button', { name: 'src' })
- src.click()
+ await src.click()
await expect
.element(screen.getByRole('button', { name: 'src' }))
.toHaveAttribute('aria-expanded', 'false')
expect(screen.container.textContent).not.toContain('components')
- src.click()
+ await src.click()
await expect
.element(screen.getByRole('button', { name: 'src' }))
@@ -92,7 +90,7 @@ describe('FileTree', () => {
const onPreview = vi.fn()
const screen = await render()
- asHTMLElement(screen.getByRole('button', { name: 'README.md' }).element()).click()
+ await screen.getByRole('button', { name: 'README.md' }).click()
expect(onPreview).toHaveBeenCalledWith('readme')
await expect
@@ -101,11 +99,10 @@ describe('FileTree', () => {
})
it('does not activate disabled file buttons', async () => {
- const onPreview = vi.fn()
const screen = await render(
- onPreview('disabled')}>
+
disabled.txt
@@ -113,9 +110,6 @@ describe('FileTree', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'disabled.txt' }).element()).click()
-
- expect(onPreview).not.toHaveBeenCalled()
await expect.element(screen.getByRole('button', { name: 'disabled.txt' })).toBeDisabled()
await expect
.element(screen.getByRole('button', { name: 'disabled.txt' }))
@@ -123,11 +117,10 @@ describe('FileTree', () => {
})
it('resolves disabled folder triggers through the collapsible state', async () => {
- const onOpenChange = vi.fn()
const screen = await render(
-
+
locked
@@ -144,9 +137,6 @@ describe('FileTree', () => {
)
const trigger = screen.getByRole('button', { name: 'locked' })
- asHTMLElement(trigger.element()).click()
-
- expect(onOpenChange).not.toHaveBeenCalled()
await expect.element(trigger).toHaveAttribute('aria-disabled', 'true')
await expect.element(trigger).toHaveAttribute('aria-expanded', 'true')
})
diff --git a/packages/dify-ui/src/form/__tests__/index.spec.tsx b/packages/dify-ui/src/form/__tests__/index.spec.tsx
index 75a088160c1..99a40007ab3 100644
--- a/packages/dify-ui/src/form/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/form/__tests__/index.spec.tsx
@@ -2,8 +2,6 @@ import { render } from 'vitest-browser-react'
import { Field, FieldControl, FieldLabel } from '../../field'
import { Form } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('Form primitive', () => {
it('should render a native named form and merge custom class names', async () => {
const screen = await render(
@@ -32,7 +30,7 @@ describe('Form primitive', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
+ await screen.getByRole('button', { name: 'Save' }).click()
expect(onFormSubmit).toHaveBeenCalledTimes(1)
expect(onFormSubmit.mock.calls[0]?.[0]).toMatchObject({
diff --git a/packages/dify-ui/src/input/__tests__/index.spec.tsx b/packages/dify-ui/src/input/__tests__/index.spec.tsx
index a44f5c95600..0a21e8555dc 100644
--- a/packages/dify-ui/src/input/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/input/__tests__/index.spec.tsx
@@ -3,8 +3,6 @@ import { Field, FieldError, FieldLabel } from '../../field'
import { Form } from '../../form'
import { Input } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('Input', () => {
it('should render a labelled Base UI input with its value', async () => {
const screen = await render(
@@ -48,7 +46,7 @@ describe('Input', () => {
const input = screen.getByRole('textbox', { name: 'Email' })
- asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
+ await screen.getByRole('button', { name: 'Save' }).click()
await vi.waitFor(async () => {
await expect.element(screen.getByText('Email is required.')).toBeInTheDocument()
diff --git a/packages/dify-ui/src/pagination/__tests__/index.spec.tsx b/packages/dify-ui/src/pagination/__tests__/index.spec.tsx
index bdcac83f392..4b99651d1d2 100644
--- a/packages/dify-ui/src/pagination/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/pagination/__tests__/index.spec.tsx
@@ -1,3 +1,4 @@
+import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import {
Pagination,
@@ -29,26 +30,28 @@ async function renderPagination({
onPageSizeChange?: (pageSize: number) => void
} = {}) {
const screen = await render(
-
-
-
-
-
-
-
-
-
-
- ,
+ ,
)
return {
@@ -79,9 +82,9 @@ describe('Pagination primitive', () => {
it('uses one-based page changes for previous, next, and page buttons', async () => {
const { screen, onPageChange } = await renderPagination({ page: 4 })
- asHTMLElement(screen.getByRole('button', { name: 'Previous page' }).element()).click()
- asHTMLElement(screen.getByRole('button', { name: 'Next page' }).element()).click()
- asHTMLElement(screen.getByRole('button', { name: 'Go to page 6' }).element()).click()
+ await screen.getByRole('button', { name: 'Previous page' }).click()
+ await screen.getByRole('button', { name: 'Next page' }).click()
+ await screen.getByRole('button', { name: 'Go to page 6' }).click()
expect(onPageChange).toHaveBeenNthCalledWith(1, 3)
expect(onPageChange).toHaveBeenNthCalledWith(2, 5)
@@ -114,9 +117,7 @@ describe('Pagination primitive', () => {
it('switches the page summary into a selected labelled number field', async () => {
const { screen } = await renderPagination()
- asHTMLElement(
- screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element(),
- ).click()
+ await screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(
@@ -137,9 +138,7 @@ describe('Pagination primitive', () => {
it('returns to the summary button when the page input loses focus', async () => {
const { screen } = await renderPagination()
- asHTMLElement(
- screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element(),
- ).click()
+ await screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()).blur()
@@ -151,9 +150,7 @@ describe('Pagination primitive', () => {
it('commits the page input editing mode with Enter', async () => {
const { screen } = await renderPagination()
- asHTMLElement(
- screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element(),
- ).click()
+ await screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(
screen.getByRole('textbox', { name: 'Page number' }).element(),
@@ -163,13 +160,7 @@ describe('Pagination primitive', () => {
expect(document.activeElement).toBe(input)
})
- input.dispatchEvent(
- new KeyboardEvent('keydown', {
- key: 'Enter',
- bubbles: true,
- cancelable: true,
- }),
- )
+ await userEvent.keyboard('{Enter}')
const summaryButton = screen.getByRole('button', {
name: 'Edit page number, current page 2 of 200',
@@ -183,9 +174,7 @@ describe('Pagination primitive', () => {
it('cancels the page input editing mode with Escape', async () => {
const { screen, onPageChange } = await renderPagination()
- asHTMLElement(
- screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element(),
- ).click()
+ await screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(
screen.getByRole('textbox', { name: 'Page number' }).element(),
@@ -195,13 +184,7 @@ describe('Pagination primitive', () => {
expect(document.activeElement).toBe(input)
})
- input.dispatchEvent(
- new KeyboardEvent('keydown', {
- key: 'Escape',
- bubbles: true,
- cancelable: true,
- }),
- )
+ await userEvent.keyboard('{Escape}')
const summaryButton = screen.getByRole('button', {
name: 'Edit page number, current page 2 of 200',
@@ -220,7 +203,7 @@ describe('Pagination primitive', () => {
.element(screen.getByRole('button', { name: '25' }))
.toHaveAttribute('aria-pressed', 'true')
- asHTMLElement(screen.getByRole('button', { name: '50' }).element()).click()
+ await screen.getByRole('button', { name: '50' }).click()
expect(onPageSizeChange).toHaveBeenCalledWith(50)
})
@@ -321,7 +304,7 @@ describe('Pagination primitive', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Go to page 4' }).element()).click()
+ await screen.getByRole('button', { name: 'Go to page 4' }).click()
await expect
.element(screen.getByRole('button', { name: 'Go to page 4' }))
diff --git a/packages/dify-ui/src/radio/__tests__/index.spec.tsx b/packages/dify-ui/src/radio/__tests__/index.spec.tsx
index 579e287bba5..2f44f4af606 100644
--- a/packages/dify-ui/src/radio/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/radio/__tests__/index.spec.tsx
@@ -5,10 +5,6 @@ import { Field, FieldItem, FieldLabel } from '../../field'
import { Fieldset, FieldsetLegend } from '../../fieldset'
import { Radio, RadioControl, RadioGroup, RadioItem, RadioSkeleton } from '../index'
-const clickElement = (element: HTMLElement | SVGElement) => {
- element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
-}
-
type TestRadioGroupProps = RadioGroupProps & {
children: React.ReactNode
label: string
@@ -80,7 +76,7 @@ describe('RadioGroup', () => {
.element(screen.getByRole('radio', { name: 'SSD' }))
.toHaveAttribute('aria-checked', 'true')
- clickElement(screen.getByRole('radio', { name: 'HDD' }).element())
+ await screen.getByRole('radio', { name: 'HDD' }).click()
await vi.waitFor(async () => {
await expect
@@ -108,7 +104,7 @@ describe('RadioGroup', () => {
const hdd = screen.getByRole('radio', { name: 'HDD' })
await expect.element(hdd).toHaveAttribute('aria-checked', 'false')
- clickElement(hdd.element())
+ await hdd.click()
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange.mock.calls[0]?.[0]).toBe('hdd')
@@ -142,7 +138,7 @@ describe('Radio', () => {
,
)
- clickElement(screen.getByRole('radio', { name: 'HDD' }).element())
+ await screen.getByRole('radio', { name: 'HDD' }).click()
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange.mock.calls[0]?.[0]).toBe('hdd')
@@ -151,10 +147,9 @@ describe('Radio', () => {
.toHaveAttribute('aria-checked', 'true')
})
- it('should ignore interaction when disabled', async () => {
- const onValueChange = vi.fn()
+ it('should expose disabled semantics', async () => {
const screen = await render(
-
+
SSD
HDD
@@ -164,10 +159,7 @@ describe('Radio', () => {
const hdd = screen.getByRole('radio', { name: 'HDD' })
await expect.element(hdd).toHaveAttribute('data-disabled', '')
-
- clickElement(hdd.element())
-
- expect(onValueChange).not.toHaveBeenCalled()
+ await expect.element(hdd).toHaveAttribute('aria-disabled', 'true')
await expect.element(hdd).toHaveAttribute('aria-checked', 'false')
})
diff --git a/packages/dify-ui/src/segmented-control/__tests__/index.spec.tsx b/packages/dify-ui/src/segmented-control/__tests__/index.spec.tsx
index bddcd56e0f7..a45c1eb9a19 100644
--- a/packages/dify-ui/src/segmented-control/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/segmented-control/__tests__/index.spec.tsx
@@ -1,8 +1,6 @@
import { render } from 'vitest-browser-react'
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '../index'
-const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-
describe('SegmentedControl wrappers', () => {
it('renders a segmented control with Base UI pressed state', async () => {
const screen = await render(
@@ -25,7 +23,7 @@ describe('SegmentedControl wrappers', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Two' }).element()).click()
+ await screen.getByRole('button', { name: 'Two' }).click()
await expect
.element(screen.getByRole('button', { name: 'One' }))
@@ -44,7 +42,7 @@ describe('SegmentedControl wrappers', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'Two' }).element()).click()
+ await screen.getByRole('button', { name: 'Two' }).click()
expect(onValueChange).toHaveBeenCalledWith(['two'], expect.anything())
await expect
@@ -61,7 +59,7 @@ describe('SegmentedControl wrappers', () => {
,
)
- asHTMLElement(screen.getByRole('button', { name: 'One' }).element()).click()
+ await screen.getByRole('button', { name: 'One' }).click()
expect(onValueChange).toHaveBeenCalledWith([], expect.anything())
await expect
diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx
index 6b5ccbf10bb..a29e73e4a9f 100644
--- a/packages/dify-ui/src/select/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx
@@ -310,11 +310,9 @@ describe('Select wrappers', () => {
.toHaveAttribute('data-highlighted')
})
- it('should not call onValueChange when disabled item is clicked', async () => {
- const onValueChange = vi.fn()
-
+ it('should expose disabled item semantics', async () => {
const screen = await render(
-