diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md
index e9c762073d..cd9485c400 100644
--- a/packages/dify-ui/README.md
+++ b/packages/dify-ui/README.md
@@ -90,6 +90,22 @@ See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for t
- `pnpm -C packages/dify-ui storybook` — Storybook on the default port. Each primitive has `index.stories.tsx`.
- `pnpm -C packages/dify-ui type-check` — `tsgo --noEmit` for this package only.
+### Disabling Animations In Tests
+
+Base UI can wait for `element.getAnimations()` to finish before it unmounts overlays, panels, and transition-driven components. Browser-based test runners can make that timing unstable, especially when tests assert final DOM state rather than animation behavior.
+
+Set the Base UI test flag in a Vitest setup file to skip those waits:
+
+```ts
+(
+ globalThis as typeof globalThis & {
+ BASE_UI_ANIMATIONS_DISABLED: boolean
+ }
+).BASE_UI_ANIMATIONS_DISABLED = true
+```
+
+`packages/dify-ui/vitest.setup.ts` already applies this for primitive tests.
+
See `[AGENTS.md](./AGENTS.md)` for:
- Component authoring rules (one-component-per-folder, `cva` + `cn`, relative imports inside the package, subpath imports from consumers).
diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx
index edbdacd203..51fccf70d8 100644
--- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx
@@ -3,19 +3,20 @@ import { toast, ToastHost } from '../index'
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
-declare global {
- // eslint-disable-next-line vars-on-top
- var BASE_UI_ANIMATIONS_DISABLED: boolean | undefined
+const dispatchToastMouseOver = (element: HTMLElement | SVGElement) => {
+ element.dispatchEvent(new MouseEvent('mouseover', {
+ bubbles: true,
+ }))
+}
+
+const dispatchToastMouseOut = (element: HTMLElement | SVGElement) => {
+ element.dispatchEvent(new MouseEvent('mouseout', {
+ bubbles: true,
+ relatedTarget: document.body,
+ }))
}
describe('@langgenius/dify-ui/toast', () => {
- beforeAll(() => {
- // Base UI waits for `requestAnimationFrame` + `getAnimations().finished`
- // before unmounting a toast. Fake timers can't reliably drive that path,
- // so short-circuit it to keep auto-dismiss assertions deterministic in CI.
- globalThis.BASE_UI_ANIMATIONS_DISABLED = true
- })
-
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
@@ -28,10 +29,6 @@ describe('@langgenius/dify-ui/toast', () => {
vi.useRealTimers()
})
- afterAll(() => {
- globalThis.BASE_UI_ANIMATIONS_DISABLED = undefined
- })
-
it('should render a success toast when called through the typed shortcut', async () => {
const screen = await render()
@@ -62,13 +59,13 @@ describe('@langgenius/dify-ui/toast', () => {
expect(document.body.querySelectorAll('[role="dialog"]')).toHaveLength(3)
expect(document.body.querySelectorAll('button[aria-label="Close notification"][aria-hidden="true"]')).toHaveLength(3)
- screen.getByRole('region', { name: 'Notifications' }).element().dispatchEvent(new MouseEvent('mouseover', {
- bubbles: true,
- }))
+ const viewport = screen.getByRole('region', { name: 'Notifications' }).element()
+ dispatchToastMouseOver(viewport)
await vi.waitFor(() => {
expect(document.body.querySelector('button[aria-label="Close notification"][aria-hidden="true"]')).not.toBeInTheDocument()
})
+ dispatchToastMouseOut(viewport)
})
it('should render a neutral toast when called directly', async () => {
@@ -115,11 +112,11 @@ describe('@langgenius/dify-ui/toast', () => {
onClose,
})
- screen.getByRole('region', { name: 'Notifications' }).element().dispatchEvent(new MouseEvent('mouseover', {
- bubbles: true,
- }))
+ const viewport = screen.getByRole('region', { name: 'Notifications' }).element()
+ dispatchToastMouseOver(viewport)
await expect.element(screen.getByRole('button', { name: 'Close notification' })).toBeInTheDocument()
+ dispatchToastMouseOut(viewport)
asHTMLElement(screen.getByRole('button', { name: 'Close notification' }).element()).click()
await vi.waitFor(() => {
@@ -128,21 +125,6 @@ describe('@langgenius/dify-ui/toast', () => {
expect(onClose).toHaveBeenCalledTimes(1)
})
- it('should auto dismiss toasts with the Base UI default timeout', async () => {
- const screen = await render()
-
- toast('Default timeout')
- await expect.element(screen.getByText('Default timeout')).toBeInTheDocument()
-
- await vi.advanceTimersByTimeAsync(4999)
- expect(document.body).toHaveTextContent('Default timeout')
-
- await vi.advanceTimersByTimeAsync(1)
- await vi.waitFor(() => {
- expect(document.body).not.toHaveTextContent('Default timeout')
- })
- })
-
it('should respect the host timeout configuration', async () => {
const screen = await render()
diff --git a/packages/dify-ui/vite.config.ts b/packages/dify-ui/vite.config.ts
index 5f3533c706..f2a2d24e57 100644
--- a/packages/dify-ui/vite.config.ts
+++ b/packages/dify-ui/vite.config.ts
@@ -11,6 +11,7 @@ export default defineConfig({
},
test: {
globals: true,
+ setupFiles: ['./vitest.setup.ts'],
browser: {
enabled: true,
provider: playwright(),
diff --git a/packages/dify-ui/vitest.setup.ts b/packages/dify-ui/vitest.setup.ts
new file mode 100644
index 0000000000..285d6e7760
--- /dev/null
+++ b/packages/dify-ui/vitest.setup.ts
@@ -0,0 +1,5 @@
+(
+ globalThis as typeof globalThis & {
+ BASE_UI_ANIMATIONS_DISABLED: boolean
+ }
+).BASE_UI_ANIMATIONS_DISABLED = true