From f697ffb7dc068e265f26fa6a2dd3e18a98068804 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Wed, 30 Sep 2026 21:52:02 +0800 Subject: [PATCH] test: stop restoring window as an undefined global Assigning globalThis.window = undefined creates an own property. On Ubuntu that shadows the host window, so later editor tests throw when ProseMirror reads window.MutationObserver. --- .../lib/ai-selection-menu-preference.test.mjs | 3 ++- .../lib/ai-space-shortcut-preference.test.mjs | 3 ++- apps/web/src/lib/app-helpers.test.mjs | 3 ++- .../src/lib/desktop-renderer-ready.test.mjs | 6 +++-- apps/web/src/lib/desktop-sync.test.mjs | 3 ++- .../lib/desktop-workspace-restore.test.mjs | 3 ++- .../lib/plugins/plugin-catalog-host.test.mjs | 16 ++++++++----- apps/web/src/lib/plugins/plugin-host.test.mjs | 16 ++++++++----- .../src/lib/plugins/plugin-lifecycle.test.mjs | 16 ++++++++----- apps/web/src/lib/repository.test.mjs | 3 ++- apps/web/src/lib/restore-test-global.mjs | 7 ++++++ apps/web/src/lib/restore-test-global.test.mjs | 23 +++++++++++++++++++ packages/shared/src/ai-assistant.test.mjs | 3 ++- 13 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 apps/web/src/lib/restore-test-global.mjs create mode 100644 apps/web/src/lib/restore-test-global.test.mjs diff --git a/apps/web/src/lib/ai-selection-menu-preference.test.mjs b/apps/web/src/lib/ai-selection-menu-preference.test.mjs index 89d89821..6ab769ee 100644 --- a/apps/web/src/lib/ai-selection-menu-preference.test.mjs +++ b/apps/web/src/lib/ai-selection-menu-preference.test.mjs @@ -9,7 +9,8 @@ import { const originalWindow = globalThis.window; afterEach(() => { - globalThis.window = originalWindow; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; }); const installWindow = () => { diff --git a/apps/web/src/lib/ai-space-shortcut-preference.test.mjs b/apps/web/src/lib/ai-space-shortcut-preference.test.mjs index 83d1444a..f0555510 100644 --- a/apps/web/src/lib/ai-space-shortcut-preference.test.mjs +++ b/apps/web/src/lib/ai-space-shortcut-preference.test.mjs @@ -9,7 +9,8 @@ import { const originalWindow = globalThis.window; afterEach(() => { - globalThis.window = originalWindow; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; }); const installWindow = () => { diff --git a/apps/web/src/lib/app-helpers.test.mjs b/apps/web/src/lib/app-helpers.test.mjs index 5f6345ba..35a072bd 100644 --- a/apps/web/src/lib/app-helpers.test.mjs +++ b/apps/web/src/lib/app-helpers.test.mjs @@ -61,7 +61,8 @@ const installLocalStorage = (initialValue = null) => { }; afterEach(() => { - globalThis.window = originalWindow; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; }); describe("search shortcut scope", () => { diff --git a/apps/web/src/lib/desktop-renderer-ready.test.mjs b/apps/web/src/lib/desktop-renderer-ready.test.mjs index 28fecc9b..f46672d3 100644 --- a/apps/web/src/lib/desktop-renderer-ready.test.mjs +++ b/apps/web/src/lib/desktop-renderer-ready.test.mjs @@ -5,8 +5,10 @@ const originalWindow = globalThis.window; const originalDocument = globalThis.document; afterEach(() => { - globalThis.window = originalWindow; - globalThis.document = originalDocument; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; + if (originalDocument === undefined) delete globalThis.document; + else globalThis.document = originalDocument; }); describe("desktop renderer readiness", () => { diff --git a/apps/web/src/lib/desktop-sync.test.mjs b/apps/web/src/lib/desktop-sync.test.mjs index 531f1b23..78f04139 100644 --- a/apps/web/src/lib/desktop-sync.test.mjs +++ b/apps/web/src/lib/desktop-sync.test.mjs @@ -38,7 +38,8 @@ describe("desktop staged resource sync", () => { release(); expect(events.filter((event) => event === "edgeever:sync-queue-changed")).toHaveLength(1); } finally { - globalThis.window = originalWindow; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; } }); diff --git a/apps/web/src/lib/desktop-workspace-restore.test.mjs b/apps/web/src/lib/desktop-workspace-restore.test.mjs index 3bab686c..721a884a 100644 --- a/apps/web/src/lib/desktop-workspace-restore.test.mjs +++ b/apps/web/src/lib/desktop-workspace-restore.test.mjs @@ -46,7 +46,8 @@ describe("desktop workspace restore", () => { try { expect(shouldRestoreDesktopWorkspace()).toBe(true); } finally { - globalThis.window = previous; + if (previous === undefined) delete globalThis.window; + else globalThis.window = previous; } }); }); diff --git a/apps/web/src/lib/plugins/plugin-catalog-host.test.mjs b/apps/web/src/lib/plugins/plugin-catalog-host.test.mjs index bf93e80f..6046f3e3 100644 --- a/apps/web/src/lib/plugins/plugin-catalog-host.test.mjs +++ b/apps/web/src/lib/plugins/plugin-catalog-host.test.mjs @@ -1,10 +1,11 @@ import { afterAll, expect, test } from "bun:test"; +import { restoreTestGlobal } from "../restore-test-global.mjs"; const originalWindow = globalThis.window; const originalDocument = globalThis.document; const originalMutationObserver = globalThis.MutationObserver; const values = new Map(); -globalThis.window = { +const stubWindow = { location: { href: "https://example.test" }, localStorage: { getItem: (key) => values.get(key) ?? null, @@ -16,7 +17,8 @@ globalThis.window = { addEventListener() {}, removeEventListener() {}, }; -globalThis.document = { +globalThis.window = stubWindow; +const stubDocument = { documentElement: { classList: { contains: () => false }, dataset: {}, @@ -24,14 +26,16 @@ globalThis.document = { removeAttribute() {}, }, }; -globalThis.MutationObserver = class { observe() {} disconnect() {} }; +globalThis.document = stubDocument; +const StubMutationObserver = class { observe() {} disconnect() {} }; +globalThis.MutationObserver = StubMutationObserver; const { EdgeEverPluginHost } = await import("./plugin-host.ts"); afterAll(() => { - globalThis.window = originalWindow; - globalThis.document = originalDocument; - globalThis.MutationObserver = originalMutationObserver; + restoreTestGlobal("window", originalWindow, stubWindow); + restoreTestGlobal("document", originalDocument, stubDocument); + restoreTestGlobal("MutationObserver", originalMutationObserver, StubMutationObserver); }); const secretStorage = { get: async () => null, set: async () => {}, remove: async () => {}, clearNamespace: async () => {} }; diff --git a/apps/web/src/lib/plugins/plugin-host.test.mjs b/apps/web/src/lib/plugins/plugin-host.test.mjs index 4b001cee..51edb997 100644 --- a/apps/web/src/lib/plugins/plugin-host.test.mjs +++ b/apps/web/src/lib/plugins/plugin-host.test.mjs @@ -1,4 +1,5 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { restoreTestGlobal } from "../restore-test-global.mjs"; const originalWindow = globalThis.window; const originalDocument = globalThis.document; @@ -8,7 +9,7 @@ const values = new Map(); const styles = new Map(); const eventListeners = new Map(); -globalThis.window = { +const stubWindow = { location: { href: "https://edgeever.example/settings" }, localStorage: { getItem: (key) => values.get(key) ?? null, @@ -20,8 +21,9 @@ globalThis.window = { addEventListener: (name, listener) => eventListeners.set(name, listener), removeEventListener: (name) => eventListeners.delete(name), }; +globalThis.window = stubWindow; -globalThis.document = { +const stubDocument = { documentElement: { classList: { contains: () => false }, dataset: {}, @@ -34,11 +36,13 @@ globalThis.document = { }, }, }; +globalThis.document = stubDocument; -globalThis.MutationObserver = class { +const StubMutationObserver = class { observe() {} disconnect() {} }; +globalThis.MutationObserver = StubMutationObserver; const { EdgeEverPluginHost, applyPluginMarkdownEdits } = await import("./plugin-host.ts"); const { sha256Hex } = await import("./github-plugin-distribution.ts"); @@ -46,9 +50,9 @@ const { withRepositoryMutationEvents } = await import("../repository-events.ts") const { results: capabilityResults } = await import('./plugin-capabilities.fixture.mjs'); afterAll(() => { - globalThis.window = originalWindow; - globalThis.document = originalDocument; - globalThis.MutationObserver = originalMutationObserver; + restoreTestGlobal("window", originalWindow, stubWindow); + restoreTestGlobal("document", originalDocument, stubDocument); + restoreTestGlobal("MutationObserver", originalMutationObserver, StubMutationObserver); }); const repository = { diff --git a/apps/web/src/lib/plugins/plugin-lifecycle.test.mjs b/apps/web/src/lib/plugins/plugin-lifecycle.test.mjs index 96c02da5..2dbbee08 100644 --- a/apps/web/src/lib/plugins/plugin-lifecycle.test.mjs +++ b/apps/web/src/lib/plugins/plugin-lifecycle.test.mjs @@ -1,16 +1,20 @@ import { afterAll, expect, test } from 'bun:test'; +import { restoreTestGlobal } from '../restore-test-global.mjs'; const originalWindow = globalThis.window; const originalDocument = globalThis.document; const originalMutationObserver = globalThis.MutationObserver; const values = new Map(); -globalThis.window = { location: { href: 'https://example.test' }, localStorage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, String(value)), removeItem: key => values.delete(key) }, addEventListener() {}, removeEventListener() {} }; -globalThis.document = { documentElement: { classList: { contains: () => false }, dataset: {}, style: { setProperty() {}, removeProperty() {} }, removeAttribute() {} } }; -globalThis.MutationObserver = class { observe() {} disconnect() {} }; +const stubWindow = { location: { href: 'https://example.test' }, localStorage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, String(value)), removeItem: key => values.delete(key) }, addEventListener() {}, removeEventListener() {} }; +const stubDocument = { documentElement: { classList: { contains: () => false }, dataset: {}, style: { setProperty() {}, removeProperty() {} }, removeAttribute() {} } }; +const StubMutationObserver = class { observe() {} disconnect() {} }; +globalThis.window = stubWindow; +globalThis.document = stubDocument; +globalThis.MutationObserver = StubMutationObserver; const { EdgeEverPluginHost } = await import('./plugin-host'); afterAll(() => { - globalThis.window = originalWindow; - globalThis.document = originalDocument; - globalThis.MutationObserver = originalMutationObserver; + restoreTestGlobal('window', originalWindow, stubWindow); + restoreTestGlobal('document', originalDocument, stubDocument); + restoreTestGlobal('MutationObserver', originalMutationObserver, StubMutationObserver); }); function setup() { values.clear(); diff --git a/apps/web/src/lib/repository.test.mjs b/apps/web/src/lib/repository.test.mjs index 27dbda62..59d46eaa 100644 --- a/apps/web/src/lib/repository.test.mjs +++ b/apps/web/src/lib/repository.test.mjs @@ -16,7 +16,8 @@ const installTestWindow = ({ hostname = "localhost" } = {}) => { location: { hostname, href: `http://${hostname}/` }, }); return () => { - globalThis.window = previousWindow; + if (previousWindow === undefined) delete globalThis.window; + else globalThis.window = previousWindow; }; }; diff --git a/apps/web/src/lib/restore-test-global.mjs b/apps/web/src/lib/restore-test-global.mjs new file mode 100644 index 00000000..78d75f4b --- /dev/null +++ b/apps/web/src/lib/restore-test-global.mjs @@ -0,0 +1,7 @@ +const restoreUnconditionally = Symbol("restore-unconditionally"); + +export const restoreTestGlobal = (key, original, installed = restoreUnconditionally) => { + if (installed !== restoreUnconditionally && globalThis[key] !== installed) return; + if (original === undefined) delete globalThis[key]; + else globalThis[key] = original; +}; diff --git a/apps/web/src/lib/restore-test-global.test.mjs b/apps/web/src/lib/restore-test-global.test.mjs new file mode 100644 index 00000000..f383ac6e --- /dev/null +++ b/apps/web/src/lib/restore-test-global.test.mjs @@ -0,0 +1,23 @@ +import { expect, test } from "bun:test"; +import { restoreTestGlobal } from "./restore-test-global.mjs"; + +test("restoreTestGlobal deletes a missing global instead of shadowing it with undefined", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "window"); + delete globalThis.window; + globalThis.window = { localStorage: {} }; + restoreTestGlobal("window", undefined, globalThis.window); + expect(Object.hasOwn(globalThis, "window")).toBe(false); + if (descriptor) Object.defineProperty(globalThis, "window", descriptor); +}); + +test("restoreTestGlobal leaves a global that another file replaced", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "document"); + const installed = { owned: true }; + const replacement = { owned: false }; + globalThis.document = installed; + globalThis.document = replacement; + restoreTestGlobal("document", undefined, installed); + expect(globalThis.document).toBe(replacement); + if (descriptor) Object.defineProperty(globalThis, "document", descriptor); + else delete globalThis.document; +}); diff --git a/packages/shared/src/ai-assistant.test.mjs b/packages/shared/src/ai-assistant.test.mjs index 1d16413b..06e0ea25 100644 --- a/packages/shared/src/ai-assistant.test.mjs +++ b/packages/shared/src/ai-assistant.test.mjs @@ -21,7 +21,8 @@ const originalWindow = globalThis.window; const originalLocalStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); afterEach(() => { - globalThis.window = originalWindow; + if (originalWindow === undefined) delete globalThis.window; + else globalThis.window = originalWindow; if (originalLocalStorage) { Object.defineProperty(globalThis, "localStorage", originalLocalStorage); } else {