From 791fc5819d71a659b59d1f769664fc5b98d6116c Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:12:23 +0800 Subject: [PATCH] test(dify-ui): disable base ui animations globally (#35467) --- packages/dify-ui/README.md | 16 ++++++ .../src/toast/__tests__/index.spec.tsx | 52 ++++++------------- packages/dify-ui/vite.config.ts | 1 + packages/dify-ui/vitest.setup.ts | 5 ++ 4 files changed, 39 insertions(+), 35 deletions(-) create mode 100644 packages/dify-ui/vitest.setup.ts 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