All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
// 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<Element>();
|
|
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 };
|