import { JSDOM } from "jsdom"; import { register } from "node:module"; // Register the loader so we can use the / prefix to load files from the src directory register(new URL("./loader.js", import.meta.url)); // Enable JSDOM const dom = new JSDOM("", { url: "http://localhost/" }); globalThis.window = dom.window; globalThis.window.scrollTo = () => {}; globalThis.document = dom.window.document; globalThis.HTMLElement = dom.window.HTMLElement; globalThis.customElements = dom.window.customElements; globalThis.CustomEvent = dom.window.CustomEvent; globalThis.Node = dom.window.Node; globalThis.DocumentFragment = dom.window.DocumentFragment; globalThis.Event = dom.window.Event; globalThis.KeyboardEvent = dom.window.KeyboardEvent; globalThis.MouseEvent = dom.window.MouseEvent; globalThis.MutationObserver = dom.window.MutationObserver; globalThis.FileReader = dom.window.FileReader; globalThis.Blob = dom.window.Blob; globalThis.getComputedStyle = dom.window.getComputedStyle; // Matches head.html template globalThis.window.env = { hostName: "localhost", gitCommit: "test", environment: "test", version: "0.0.0", pluginSdkFileName: "pluginSdk.test.js", oauthScopes: "atproto", oauthOptionalScopes: "", playwright: false, useConfidentialOauth: false, }; // Node has its own global navigator; alias JSDOM's so code under test and // tests (which mock properties on window.navigator) see the same object. Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator, configurable: true, writable: true, }); // Mock requestAnimationFrame const realSetTimeout = globalThis.setTimeout; globalThis.requestAnimationFrame = (callback) => realSetTimeout(callback, 0); globalThis.window.requestAnimationFrame = globalThis.requestAnimationFrame; // Mock matchMedia globalThis.window.matchMedia = (query) => ({ matches: false, media: query, onchange: null, addListener: () => {}, removeListener: () => {}, addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => {}, }); class IntersectionObserver { observe() {} unobserve() {} disconnect() {} } globalThis.IntersectionObserver = IntersectionObserver; globalThis.window.HTMLDialogElement.prototype.showModal = function () { this.setAttribute("open", ""); }; globalThis.window.HTMLDialogElement.prototype.close = function () { this.removeAttribute("open"); this.dispatchEvent(new globalThis.Event("close")); }; globalThis.window.HTMLElement.prototype.showPopover = function () {}; globalThis.window.HTMLElement.prototype.hidePopover = function () {}; globalThis.window.HTMLMediaElement.prototype.play = function () { return Promise.resolve(); }; globalThis.window.HTMLMediaElement.prototype.pause = function () {}; globalThis.window.HTMLMediaElement.prototype.load = function () {}; // Prevent network requests. We can mock this in individual tests as needed. delete globalThis.fetch; // JSDOM's window.crypto lacks subtle; swap in node's webcrypto. if (!globalThis.window.crypto?.subtle) { Object.defineProperty(globalThis.window, "crypto", { value: globalThis.crypto, configurable: true, writable: true, }); } class LocalStorageStub { constructor() { this._store = {}; } getItem(key) { return key in this._store ? this._store[key] : null; } setItem(key, value) { this._store[key] = String(value); } removeItem(key) { delete this._store[key]; } get length() { return Object.keys(this._store).length; } key(index) { return Object.keys(this._store)[index] ?? null; } clear() { this._store = {}; } } globalThis.localStorage = new LocalStorageStub(); globalThis.Element = globalThis.window.Element; // Components exercise scroll locking without the app bootstrap, so supply the // same active-page boundary through the test fixture. const { scrollLocks } = await import("/js/scrollLocks.js"); scrollLocks.setContainerProvider(() => document.querySelector(".page-visible")); class HighlightStub { constructor(...ranges) { this._ranges = new Set(ranges); } add(range) { this._ranges.add(range); } delete(range) { return this._ranges.delete(range); } clear() { this._ranges.clear(); } get size() { return this._ranges.size; } } globalThis.Highlight = HighlightStub; globalThis.window.Highlight = HighlightStub; if (typeof globalThis.window.CSS === "undefined") { globalThis.window.CSS = {}; } globalThis.window.CSS.highlights = new Map(); globalThis.CSS = globalThis.window.CSS; // Real emoji element makes a fetch / uses indexeddb, so stub it class EmojiPickerStub extends globalThis.window.HTMLElement {} if (!globalThis.customElements.get("emoji-picker")) { globalThis.customElements.define("emoji-picker", EmojiPickerStub); } // Global per-test cleanup: emptying the body disconnects custom elements // (whose disconnectedCallback clears their timers) and cleanupToasts // cancels toast timers, so no test leaves timers pending on the event loop. const { afterEach } = await import("node:test"); const { cleanupToasts } = await import("/js/toasts.js"); afterEach(() => { document.body.replaceChildren(); cleanupToasts(); });