// Runs before every test file: DOM matchers, the English catalog, and the // browser APIs jsdom leaves out that the app reaches for at render time. import "@testing-library/jest-dom/vitest"; import { cleanup, configure } from "@testing-library/react"; import { afterEach, beforeEach, vi } from "vitest"; import { i18n } from "@lingui/core"; import { messages } from "../locales/en.po"; import { FakeWebSocket } from "./ws.ts"; import { resetFetch } from "./fetch.ts"; // Lazy chunks and the app's own debounces (a link preview waits 600ms) make // the default 1s too tight once every file runs in parallel. configure({ asyncUtilTimeout: 4000 }); i18n.load("en", messages); i18n.activate("en"); class FakeIntersectionObserver { static instances: FakeIntersectionObserver[] = []; /** * Whether observed elements are on screen. When set, a new observer reports * its element as intersecting right after observe(), as a browser's does — * which is what re-triggers a sentinel that never left the view. */ static inView = false; elements = new Set(); callback: IntersectionObserverCallback; constructor(callback: IntersectionObserverCallback) { this.callback = callback; FakeIntersectionObserver.instances.push(this); } observe(el: Element) { this.elements.add(el); if (!FakeIntersectionObserver.inView) return; setTimeout(() => { if (!this.elements.has(el)) return; this.callback( [{ isIntersecting: true, target: el } as unknown as IntersectionObserverEntry], this as unknown as IntersectionObserver, ); }); } unobserve(el: Element) { this.elements.delete(el); } disconnect() { this.elements.clear(); } takeRecords() { return []; } } class FakeResizeObserver { observe() {} unobserve() {} disconnect() {} } Object.assign(globalThis, { IntersectionObserver: FakeIntersectionObserver, ResizeObserver: FakeResizeObserver, }); if (!globalThis.matchMedia) { globalThis.matchMedia = (query: string) => ({ matches: false, media: query, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, dispatchEvent: () => false, }) as MediaQueryList; } Element.prototype.scrollIntoView ??= function () {}; Element.prototype.animate ??= function () { return { cancel() {}, finished: Promise.resolve() } as unknown as Animation; }; Element.prototype.getAnimations ??= () => []; globalThis.scrollTo = () => {}; HTMLMediaElement.prototype.play = function () { return Promise.resolve(); }; HTMLMediaElement.prototype.pause = function () {}; HTMLMediaElement.prototype.load = function () {}; // The emoji picker probes emoji support on a canvas; jsdom has none. HTMLCanvasElement.prototype.getContext = (() => null) as never; // jsdom has no layout; execCommand("insertText") is how the editor types. document.execCommand ??= () => false; beforeEach(() => { resetFetch(); FakeWebSocket.reset(); vi.stubGlobal("WebSocket", FakeWebSocket); FakeIntersectionObserver.instances = []; FakeIntersectionObserver.inView = false; }); afterEach(() => { cleanup(); localStorage.clear(); sessionStorage.clear(); document.documentElement.removeAttribute("style"); }); export { FakeIntersectionObserver };