/** * 全局测试配置 * 主要为后端测试提供基础环境 * 组件测试使用各自的 test-utils.tsx */ // Set up jsdom for ALL tests (both backend and frontend) import { JSDOM } from "jsdom"; const dom = new JSDOM("", { pretendToBeVisual: true, url: "http://localhost", }); globalThis.document = dom.window.document; globalThis.window = dom.window as unknown as typeof globalThis & Window; globalThis.navigator = dom.window.navigator; globalThis.HTMLElement = dom.window.HTMLElement; globalThis.Element = dom.window.Element; globalThis.getComputedStyle = dom.window.getComputedStyle; // Ensure document.body exists if (!globalThis.document.body) { const body = globalThis.document.createElement("body"); globalThis.document.documentElement.appendChild(body); } // CRITICAL: Set up polyfills BEFORE any other imports // This ensures @testing-library/react sees these when it loads // IE-style event handling polyfill (React fallback) const nodeProto = dom.window.Node.prototype; const elementProto = dom.window.Element.prototype; const htmlElementProto = dom.window.HTMLElement.prototype; const noop = () => undefined; const attachEventFn = noop; const detachEventFn = noop; Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true }); Object.defineProperty(nodeProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true }); Object.defineProperty(elementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true }); Object.defineProperty(elementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true }); Object.defineProperty(htmlElementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true }); Object.defineProperty(htmlElementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true }); // Other polyfills globalThis.ResizeObserver = class { disconnect() { return undefined; } observe() { return undefined; } unobserve() { return undefined; } }; globalThis.MutationObserver = class { disconnect() { return undefined; } observe() { return undefined; } takeRecords() { return []; } unobserve() { return undefined; } }; globalThis.IntersectionObserver = class { disconnect() { return undefined; } observe() { return undefined; } takeRecords() { return []; } unobserve() { return undefined; } } as unknown as typeof IntersectionObserver; globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16); globalThis.cancelAnimationFrame = (id: number) => clearTimeout(id); Object.defineProperty(dom.window, "matchMedia", { value: (query: string) => ({ addEventListener: noop, addListener: noop, dispatchEvent: () => true, matches: false, media: query, onchange: null, removeEventListener: noop, removeListener: noop, }), writable: true, }); dom.window.Element.prototype.scrollTo = noop; dom.window.Element.prototype.scrollIntoView = noop; Object.defineProperty(dom.window, "customElements", { value: { define: noop, get: () => undefined, }, writable: true, }); globalThis.customElements = dom.window.customElements; // Mock @number-flow/react globally (custom elements not supported in jsdom) import { afterEach, mock } from "bun:test"; import { createElement } from "react"; void mock.module("@number-flow/react", () => { const NumberFlow = () => createElement("span", { "data-testid": "number-flow" }); const NumberFlowGroup = ({ children }: { children: unknown }) => children; return { default: NumberFlow, NumberFlowGroup }; }); afterEach(() => { document.body.innerHTML = ""; });