test: give editor tests a document head and its window

This commit is contained in:
tianma-if committed 2026-09-30 22:24:01 +08:00
1 parent f7d7e8e72c
commit b508ff0974
3 files changed
+94 -15

No files matched your search

+21 -5
View File
@@ -1,12 +1,28 @@
const restoreUnconditionally = Symbol("restore-unconditionally");
const noop = () => {};
// Linux Bun exposes `document` without a `window` binding. Test files then
// install plain objects. ProseMirror and TipTap read `window.MutationObserver`
// and `window.addEventListener` from that object, so a leftover stub needs
// those methods. Do not create a window when none is installed: assigning one
// is unnecessary on hosts whose editor already resolves `window` elsewhere.
// Linux Bun can be left with a plain window object, or with a linkedom
// document whose view is not installed as `window`. TipTap reads
// `window.addEventListener` and appends CSS to `document.head`.
export const ensureTestDocumentHead = () => {
const doc = globalThis.document;
if (!doc || typeof doc.createElement !== "function" || typeof doc.getElementsByTagName !== "function") return;
if (doc.getElementsByTagName("head")[0]) return;
const head = doc.createElement("head");
const html = doc.documentElement;
if (html && typeof html.insertBefore === "function") {
html.insertBefore(head, html.firstChild ?? null);
return;
}
if (typeof doc.appendChild === "function") doc.appendChild(head);
};
export const ensureTestWindowDom = () => {
const view = globalThis.document?.defaultView;
if (view && typeof globalThis.window?.addEventListener !== "function") {
globalThis.window = view;
}
ensureTestDocumentHead();
const current = globalThis.window;
if (!current || typeof current !== "object") return;
if (typeof current.addEventListener !== "function") current.addEventListener = noop;
+45 -2
View File
@@ -1,9 +1,8 @@
import { expect, test } from "bun:test";
import { ensureTestWindowDom, restoreTestGlobal } from "./restore-test-global.mjs";
import { ensureTestDocumentHead, ensureTestWindowDom, restoreTestGlobal } from "./restore-test-global.mjs";
test("restoreTestGlobal keeps a window object when nothing was there before", () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
delete globalThis.window;
const stub = { localStorage: {} };
globalThis.window = stub;
restoreTestGlobal("window", undefined, stub);
@@ -26,6 +25,50 @@ test("restoreTestGlobal leaves a global that another file replaced", () => {
else delete globalThis.document;
});
test("ensureTestDocumentHead inserts a head when the document has none", () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
const heads = [];
const html = {
firstChild: null,
insertBefore(node) {
heads.push(node);
},
};
globalThis.document = {
documentElement: html,
createElement: (name) => ({ nodeName: name, appendChild() {} }),
getElementsByTagName: (name) => (name === "head" ? heads : []),
};
ensureTestDocumentHead();
expect(heads).toHaveLength(1);
ensureTestDocumentHead();
expect(heads).toHaveLength(1);
if (descriptor) Object.defineProperty(globalThis, "document", descriptor);
else delete globalThis.document;
});
test("ensureTestWindowDom uses the document view when the leftover window cannot listen", () => {
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
const view = {
addEventListener() {},
removeEventListener() {},
};
globalThis.document = {
defaultView: view,
documentElement: { insertBefore() {} },
createElement: (name) => ({ nodeName: name }),
getElementsByTagName: () => [{}],
};
globalThis.window = { edgeeverDesktop: { isAvailable: true } };
ensureTestWindowDom();
expect(globalThis.window).toBe(view);
if (windowDescriptor) Object.defineProperty(globalThis, "window", windowDescriptor);
else delete globalThis.window;
if (documentDescriptor) Object.defineProperty(globalThis, "document", documentDescriptor);
else delete globalThis.document;
});
test("ensureTestWindowDom fills event methods on a leftover window", () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
const stub = { edgeeverDesktop: { isAvailable: true } };
+28 -8
View File
@@ -1,19 +1,39 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { parseHTML } from "../../../../node_modules/.bun/linkedom@0.18.13/node_modules/linkedom/esm/index.js";
const window = parseHTML("<!DOCTYPE html><html><body></body></html>");
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
const originalNode = Object.getOwnPropertyDescriptor(globalThis, "Node");
const originalHTMLElement = Object.getOwnPropertyDescriptor(globalThis, "HTMLElement");
const originalGetComputedStyle = Object.getOwnPropertyDescriptor(globalThis, "getComputedStyle");
const linkedomWindow = parseHTML("<!DOCTYPE html><html><head></head><body></body></html>");
try {
Object.defineProperty(window.document, "compatMode", { value: "CSS1Compat" });
Object.defineProperty(linkedomWindow.document, "compatMode", { value: "CSS1Compat" });
} catch {
// linkedom may already expose a read-only quirks flag. KaTeX still renders.
}
globalThis.document = window.document;
globalThis.Node = window.Node;
globalThis.HTMLElement = window.HTMLElement;
globalThis.getComputedStyle = typeof window.getComputedStyle === "function"
? window.getComputedStyle.bind(window)
globalThis.window = linkedomWindow;
globalThis.document = linkedomWindow.document;
globalThis.Node = linkedomWindow.Node;
globalThis.HTMLElement = linkedomWindow.HTMLElement;
globalThis.getComputedStyle = typeof linkedomWindow.getComputedStyle === "function"
? linkedomWindow.getComputedStyle.bind(linkedomWindow)
: () => ({ getPropertyValue: () => "" });
const restoreGlobal = (name, descriptor) => {
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
else delete globalThis[name];
};
afterAll(() => {
restoreGlobal("window", originalWindow);
restoreGlobal("document", originalDocument);
restoreGlobal("Node", originalNode);
restoreGlobal("HTMLElement", originalHTMLElement);
restoreGlobal("getComputedStyle", originalGetComputedStyle);
});
const css = (element) => (element?.style.cssText ?? "").replace(/:\s+/g, ":");
let preparePublishArticle;