Files
dify/web/vitest.setup.ts
Novice 89b76d6c33 Merge commit '657eeb65' into sandboxed-agent-rebase
Made-with: Cursor

# Conflicts:
#	api/core/agent/cot_chat_agent_runner.py
#	api/core/agent/fc_agent_runner.py
#	api/core/memory/token_buffer_memory.py
#	api/core/variables/segments.py
#	api/core/workflow/file/file_manager.py
#	api/core/workflow/nodes/agent/agent_node.py
#	api/core/workflow/nodes/llm/llm_utils.py
#	api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py
#	api/core/workflow/workflow_entry.py
#	api/factories/variable_factory.py
#	api/pyproject.toml
#	api/services/variable_truncator.py
#	api/uv.lock
#	web/app/components/app/app-publisher/index.tsx
#	web/app/components/app/overview/settings/index.tsx
#	web/app/components/apps/app-card.tsx
#	web/app/components/apps/index.tsx
#	web/app/components/apps/list.tsx
#	web/app/components/base/chat/chat-with-history/header-in-mobile.tsx
#	web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
#	web/app/components/base/features/new-feature-panel/file-upload/setting-content.tsx
#	web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx
#	web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx
#	web/app/components/base/message-log-modal/index.tsx
#	web/app/components/base/switch/index.tsx
#	web/app/components/base/tab-slider-plain/index.tsx
#	web/app/components/explore/try-app/app-info/index.tsx
#	web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
#	web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/required-switch.tsx
#	web/app/components/workflow/nodes/llm/panel.tsx
#	web/contract/router.ts
#	web/eslint-suppressions.json
#	web/i18n/fa-IR/workflow.json
2026-03-19 17:38:56 +08:00

175 lines
5.4 KiB
TypeScript

import { act, cleanup } from '@testing-library/react'
import { mockAnimationsApi, mockResizeObserver } from 'jsdom-testing-mocks'
import * as React from 'react'
import '@testing-library/jest-dom/vitest'
import 'vitest-canvas-mock'
mockResizeObserver()
// Mock Web Animations API for Headless UI
mockAnimationsApi()
// Suppress act() warnings from @headlessui/react internal Transition component
// These warnings are caused by Headless UI's internal async state updates, not our code
const originalConsoleError = console.error
console.error = (...args: unknown[]) => {
// Check all arguments for the Headless UI TransitionRootFn act warning
const fullMessage = args.map(arg => (typeof arg === 'string' ? arg : '')).join(' ')
if (fullMessage.includes('TransitionRootFn') && fullMessage.includes('not wrapped in act'))
return
originalConsoleError.apply(console, args)
}
// Fix for @headlessui/react compatibility with happy-dom
// headlessui tries to override focus properties which may be read-only in happy-dom
if (typeof window !== 'undefined') {
// Provide a minimal animations API polyfill before @headlessui/react boots
if (typeof Element !== 'undefined' && !Element.prototype.getAnimations)
Element.prototype.getAnimations = () => []
if (!document.getAnimations)
document.getAnimations = () => []
const ensureWritable = (target: object, prop: string) => {
const descriptor = Object.getOwnPropertyDescriptor(target, prop)
if (descriptor && !descriptor.writable) {
const original = descriptor.value ?? descriptor.get?.call(target)
Object.defineProperty(target, prop, {
value: typeof original === 'function' ? original : vi.fn(),
writable: true,
configurable: true,
})
}
}
ensureWritable(window, 'focus')
ensureWritable(HTMLElement.prototype, 'focus')
}
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class {
observe() {
return undefined
}
unobserve() {
return undefined
}
disconnect() {
return undefined
}
}
}
// Mock IntersectionObserver for tests
if (typeof globalThis.IntersectionObserver === 'undefined') {
globalThis.IntersectionObserver = class {
readonly root: Element | Document | null = null
readonly rootMargin: string = ''
readonly thresholds: ReadonlyArray<number> = []
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) { /* noop */ }
observe() { /* noop */ }
unobserve() { /* noop */ }
disconnect() { /* noop */ }
takeRecords(): IntersectionObserverEntry[] { return [] }
}
}
// Mock Element.scrollIntoView for tests (not available in happy-dom/jsdom)
if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView)
Element.prototype.scrollIntoView = function () { /* noop */ }
afterEach(async () => {
// Wrap cleanup in act() to flush pending React scheduler work
// This prevents "window is not defined" errors from React 19's scheduler
// which uses setImmediate/MessageChannel that can fire after jsdom cleanup
await act(async () => {
cleanup()
})
})
// mock next/image to avoid width/height requirements for data URLs
vi.mock('next/image')
// mock foxact/use-clipboard - not available in test environment
vi.mock('foxact/use-clipboard', () => ({
useClipboard: () => ({
copy: vi.fn(),
copied: false,
}),
}))
// mock zustand - auto-resets all stores after each test
// Based on official Zustand testing guide: https://zustand.docs.pmnd.rs/guides/testing
vi.mock('zustand')
// mock react-i18next
vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
const { createReactI18nextMock } = await import('./test/i18n-mock')
return {
...actual,
...createReactI18nextMock(),
}
})
// Mock FloatingPortal to render children in the normal DOM flow
vi.mock('@floating-ui/react', async () => {
const actual = await vi.importActual('@floating-ui/react')
return {
...actual,
FloatingPortal: ({ children }: { children: React.ReactNode }) => React.createElement('div', { 'data-floating-ui-portal': true }, children),
}
})
// mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock localStorage for testing
const createMockLocalStorage = () => {
const storage: Record<string, string> = {}
return {
getItem: vi.fn((key: string) => storage[key] || null),
setItem: vi.fn((key: string, value: string) => {
storage[key] = value
}),
removeItem: vi.fn((key: string) => {
delete storage[key]
}),
clear: vi.fn(() => {
Object.keys(storage).forEach(key => delete storage[key])
}),
get storage() { return { ...storage } },
}
}
let mockLocalStorage: ReturnType<typeof createMockLocalStorage> = createMockLocalStorage()
Object.defineProperty(globalThis, 'localStorage', {
value: mockLocalStorage,
writable: true,
configurable: true,
})
beforeEach(() => {
vi.clearAllMocks()
mockLocalStorage = createMockLocalStorage()
Object.defineProperty(globalThis, 'localStorage', {
value: mockLocalStorage,
writable: true,
configurable: true,
})
})