test: use happy dom (#34154)

This commit is contained in:
Stephen Zhou
2026-03-27 09:46:19 +08:00
committed by GitHub
parent 368896d84d
commit 07f4950cb3
33 changed files with 306 additions and 157 deletions

View File

@@ -1,14 +1,8 @@
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
@@ -77,24 +71,10 @@ if (typeof globalThis.IntersectionObserver === 'undefined') {
}
}
// Mock Element.scrollIntoView for tests (not available in happy-dom/jsdom)
if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView)
Element.prototype.scrollIntoView = function () { /* noop */ }
// Mock DOMRect.fromRect for tests (not available in jsdom)
if (typeof DOMRect !== 'undefined' && typeof (DOMRect as typeof DOMRect & { fromRect?: unknown }).fromRect !== 'function') {
(DOMRect as typeof DOMRect & { fromRect: (rect?: DOMRectInit) => DOMRect }).fromRect = (rect = {}) => new DOMRect(
rect.x ?? 0,
rect.y ?? 0,
rect.width ?? 0,
rect.height ?? 0,
)
}
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
// which uses setImmediate/MessageChannel that can fire after DOM cleanup
await act(async () => {
cleanup()
})
@@ -131,19 +111,97 @@ vi.mock('@floating-ui/react', async () => {
}
})
// 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(),
})),
vi.mock('@monaco-editor/react', () => {
const createEditorMock = () => {
const focusListeners: Array<() => void> = []
const blurListeners: Array<() => void> = []
return {
getContentHeight: vi.fn(() => 56),
onDidFocusEditorText: vi.fn((listener: () => void) => {
focusListeners.push(listener)
return { dispose: vi.fn() }
}),
onDidBlurEditorText: vi.fn((listener: () => void) => {
blurListeners.push(listener)
return { dispose: vi.fn() }
}),
layout: vi.fn(),
getAction: vi.fn(() => ({ run: vi.fn() })),
getModel: vi.fn(() => ({
getLineContent: vi.fn(() => ''),
})),
getPosition: vi.fn(() => ({ lineNumber: 1, column: 1 })),
deltaDecorations: vi.fn(() => []),
focus: vi.fn(() => {
focusListeners.forEach(listener => listener())
}),
setPosition: vi.fn(),
revealLine: vi.fn(),
trigger: vi.fn(),
__blur: () => {
blurListeners.forEach(listener => listener())
},
}
}
const monacoMock = {
editor: {
setTheme: vi.fn(),
defineTheme: vi.fn(),
},
Range: class {
startLineNumber: number
startColumn: number
endLineNumber: number
endColumn: number
constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
this.startLineNumber = startLineNumber
this.startColumn = startColumn
this.endLineNumber = endLineNumber
this.endColumn = endColumn
}
},
}
const MonacoEditor = ({
value = '',
onChange,
onMount,
options,
}: {
value?: string
onChange?: (value: string | undefined) => void
onMount?: (editor: ReturnType<typeof createEditorMock>, monaco: typeof monacoMock) => void
options?: { readOnly?: boolean }
}) => {
const editorRef = React.useRef<ReturnType<typeof createEditorMock> | null>(null)
if (!editorRef.current)
editorRef.current = createEditorMock()
React.useEffect(() => {
onMount?.(editorRef.current!, monacoMock)
}, [onMount])
return React.createElement('textarea', {
'data-testid': 'monaco-editor',
'readOnly': options?.readOnly,
value,
'onChange': (event: React.ChangeEvent<HTMLTextAreaElement>) => onChange?.(event.target.value),
'onFocus': () => editorRef.current?.focus(),
'onBlur': () => editorRef.current?.__blur(),
})
}
return {
__esModule: true,
default: MonacoEditor,
Editor: MonacoEditor,
loader: {
config: vi.fn(),
init: vi.fn().mockResolvedValue(monacoMock),
},
}
})
// Mock localStorage for testing