+51
![autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)

![gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)






qiuqiua
GitHub
QuantumGhost
盐粒 Yanli
wangxiaolei
Stephen Zhou
gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Cursx
autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
lif
非法操作
Asuka Minato
fenglin
qiaofenglin
-LAN-
TomoOkuyama
Tomo Okuyama
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
zyssyz123
hj24
Coding On Star
CodingOnStar
yyh
Xiangxuan Qu
fghpdf
coopercoder
zhaiguangpeng
Junyan Qin
E.G
GlobalStar117
Claude Haiku 4.5
CodingOnStar
crazywoola
heyszt
NeatGuyCoding
Yeuoly
zxhlyh
moonpanda
warlocgao
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
claude[bot] <41898282+claude[bot]@users.noreply.github.com>
KVOJJJin
eux
bangjiehan
FFXN
Jyong
Nie Ronghua
JQSevenMiao
jiasiqi
Seokrin Taron Sung
CrabSAMA
Copilot
yihong
Joel
Wu Tianwei
yessenia
Jax
niveshdandyan
OSS Contributor
niveshdandyan
Sean Kenneth Doherty
9ef6b90843
Signed-off-by: majiayu000 <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: NeatGuyCoding <[email protected]> Signed-off-by: -LAN- <[email protected]> Signed-off-by: yihong0618 <[email protected]> Co-authored-by: QuantumGhost <[email protected]> Co-authored-by: 盐粒 Yanli <[email protected]> Co-authored-by: wangxiaolei <[email protected]> Co-authored-by: Stephen Zhou <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Cursx <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <[email protected]> Co-authored-by: 非法操作 <[email protected]> Co-authored-by: Asuka Minato <[email protected]> Co-authored-by: fenglin <[email protected]> Co-authored-by: qiaofenglin <[email protected]> Co-authored-by: -LAN- <[email protected]> Co-authored-by: TomoOkuyama <[email protected]> Co-authored-by: Tomo Okuyama <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zyssyz123 <[email protected]> Co-authored-by: hj24 <[email protected]> Co-authored-by: Coding On Star <[email protected]> Co-authored-by: CodingOnStar <[email protected]> Co-authored-by: yyh <[email protected]> Co-authored-by: Xiangxuan Qu <[email protected]> Co-authored-by: fghpdf <[email protected]> Co-authored-by: coopercoder <[email protected]> Co-authored-by: zhaiguangpeng <[email protected]> Co-authored-by: Junyan Qin (Chin) <[email protected]> Co-authored-by: E.G <[email protected]> Co-authored-by: GlobalStar117 <[email protected]> Co-authored-by: Claude Haiku 4.5 <[email protected]> Co-authored-by: CodingOnStar <[email protected]> Co-authored-by: crazywoola <[email protected]> Co-authored-by: heyszt <[email protected]> Co-authored-by: NeatGuyCoding <[email protected]> Co-authored-by: Yeuoly <[email protected]> Co-authored-by: zxhlyh <[email protected]> Co-authored-by: moonpanda <[email protected]> Co-authored-by: warlocgao <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: KVOJJJin <[email protected]> Co-authored-by: eux <[email protected]> Co-authored-by: bangjiehan <[email protected]> Co-authored-by: FFXN <[email protected]> Co-authored-by: Jyong <[email protected]> Co-authored-by: Nie Ronghua <[email protected]> Co-authored-by: JQSevenMiao <[email protected]> Co-authored-by: jiasiqi <[email protected]> Co-authored-by: Seokrin Taron Sung <[email protected]> Co-authored-by: CrabSAMA <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: yihong <[email protected]> Co-authored-by: Joel <[email protected]> Co-authored-by: Wu Tianwei <[email protected]> Co-authored-by: yessenia <[email protected]> Co-authored-by: Jax <[email protected]> Co-authored-by: niveshdandyan <[email protected]> Co-authored-by: OSS Contributor <[email protected]> Co-authored-by: niveshdandyan <[email protected]> Co-authored-by: Sean Kenneth Doherty <[email protected]>
159 lines
4.9 KiB
TypeScript
159 lines
4.9 KiB
TypeScript
import { act, cleanup } from '@testing-library/react'
|
|
import { mockAnimationsApi, mockResizeObserver } from 'jsdom-testing-mocks'
|
|
import '@testing-library/jest-dom/vitest'
|
|
|
|
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 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>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockLocalStorage = createMockLocalStorage()
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
value: mockLocalStorage,
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
})
|