test(webui): restore browser globals after tests

This commit is contained in:
italic-jinxin
2026-09-02 23:03:08 +08:00
parent 19bb5fc91c
commit c8fd3f5ab7
5 changed files with 62 additions and 28 deletions

View File

@@ -30,22 +30,28 @@ import {
} from "./api";
const originalFetch = globalThis.fetch;
const originalSessionStorage = globalThis.sessionStorage;
const restoreBrowserGlobals: Array<() => void> = [];
function installSessionToken(token: string) {
replaceBrowserGlobal(
"sessionStorage",
createMemoryStorage(token ? { ironclaw_token: token } : {}),
restoreBrowserGlobals.push(
replaceBrowserGlobal(
"sessionStorage",
createMemoryStorage(token ? { ironclaw_token: token } : {}),
),
);
}
function installWindowOrigin(origin: string) {
replaceBrowserGlobal("window", { location: { origin } });
restoreBrowserGlobals.push(
replaceBrowserGlobal("window", { location: { origin } }),
);
}
afterEach(() => {
globalThis.fetch = originalFetch;
globalThis.sessionStorage = originalSessionStorage;
while (restoreBrowserGlobals.length > 0) {
restoreBrowserGlobals.pop()?.();
}
});
function withCryptoGlobal(replacement, run) {
@@ -185,6 +191,7 @@ test("listThreads passes pagination and candidate-thread filters", async () => {
const calls = [];
const controller = new AbortController();
installSessionToken("token-1");
installWindowOrigin("http://localhost");
globalThis.fetch = async (path, options) => {
calls.push({ path, options });
return new Response(JSON.stringify({ threads: [] }), {
@@ -484,15 +491,18 @@ test("fetchAttachmentDataUrl returns a data URL and never mints a blob URL", asy
globalThis.URL.createObjectURL = () => {
throw new Error("blob: URLs violate the SPA CSP img-src 'self' data:");
};
replaceBrowserGlobal("FileReader", class FakeFileReader {
result = "";
onload: (() => void) | null = null;
const restoreFileReader = replaceBrowserGlobal(
"FileReader",
class FakeFileReader {
result = "";
onload: (() => void) | null = null;
readAsDataURL() {
this.result = "data:image/png;base64,AQIDBA==";
if (this.onload) this.onload();
}
});
readAsDataURL() {
this.result = "data:image/png;base64,AQIDBA==";
if (this.onload) this.onload();
}
},
);
try {
const url = await fetchAttachmentDataUrl(
@@ -501,6 +511,7 @@ test("fetchAttachmentDataUrl returns a data URL and never mints a blob URL", asy
assert.ok(typeof url === "string" && url.startsWith("data:"), `expected a data URL, got ${url}`);
} finally {
globalThis.URL.createObjectURL = priorCreateObjectURL;
restoreFileReader();
}
});

View File

@@ -7,7 +7,7 @@
// asset bundle, so this file is never served to the browser.
import assert from "node:assert/strict";
import { beforeEach, test } from "vitest";
import { afterEach, beforeEach, test } from "vitest";
import {
createMemoryStorage,
replaceBrowserGlobal,
@@ -23,9 +23,11 @@ import {
// Minimal localStorage stub. The store reads `window.localStorage` lazily, so
// installing it on the global before the calls is enough.
let restoreWindow: (() => void) | null = null;
function installStorage() {
const storage = createMemoryStorage();
replaceBrowserGlobal("window", { localStorage: storage });
restoreWindow = replaceBrowserGlobal("window", { localStorage: storage });
return storage;
}
@@ -36,6 +38,11 @@ beforeEach(() => {
clearAllPins();
});
afterEach(() => {
restoreWindow?.();
restoreWindow = null;
});
test("togglePin round-trips with isPinned", () => {
assert.equal(isPinned("t1"), false);
togglePin("t1");
@@ -96,7 +103,8 @@ test("clearAllPins resets the set and removes pin keys but leaves others", () =>
});
test("storage failures are swallowed (in-memory still works)", () => {
replaceBrowserGlobal("window", {
restoreWindow?.();
restoreWindow = replaceBrowserGlobal("window", {
localStorage: {
getItem: () => {
throw new Error("quota / private mode");

View File

@@ -7,7 +7,7 @@
// carries. Everything else is pure.
import assert from "node:assert/strict";
import { beforeAll as before, test } from "vitest";
import { afterAll as after, beforeAll as before, test } from "vitest";
import { replaceBrowserGlobal } from "../../../test-support/browser-mocks";
@@ -21,6 +21,8 @@ import {
toWireAttachment,
} from "./attachments";
let restoreFileReader: (() => void) | null = null;
before(() => {
class FakeFileReader {
result: string | ArrayBuffer | null = null;
@@ -33,7 +35,12 @@ before(() => {
});
}
}
replaceBrowserGlobal("FileReader", FakeFileReader);
restoreFileReader = replaceBrowserGlobal("FileReader", FakeFileReader);
});
after(() => {
restoreFileReader?.();
restoreFileReader = null;
});
// A fake `File` carrying its own data URL so the stubbed reader is

View File

@@ -7,7 +7,7 @@
// asset bundle, so this file is never served to the browser.
import assert from "node:assert/strict";
import { beforeEach, test } from "vitest";
import { afterEach, beforeEach, test } from "vitest";
import { setAuthScope } from "../../../lib/auth-scope";
import {
createMemoryStorage,
@@ -24,9 +24,11 @@ import {
// Minimal localStorage stub — the store reads `window.localStorage` lazily
// inside each function, so installing it on the global before the calls is
// enough (the module never touches storage at import time).
let restoreWindow: (() => void) | null = null;
function installStorage() {
const storage = createMemoryStorage();
replaceBrowserGlobal("window", { localStorage: storage });
restoreWindow = replaceBrowserGlobal("window", { localStorage: storage });
return storage;
}
@@ -35,6 +37,11 @@ beforeEach(() => {
setAuthScope(null);
});
afterEach(() => {
restoreWindow?.();
restoreWindow = null;
});
test("drafts are isolated per authenticated user across a session switch", () => {
// Regression: signing out and a different user signing into the same tab
// must not restore the previous user's unsent draft (the new-conversation
@@ -118,7 +125,8 @@ test("storage failures are swallowed (best-effort persistence)", () => {
storage.removeItem = () => {
throw new Error("quota / private mode");
};
replaceBrowserGlobal("window", { localStorage: storage });
restoreWindow?.();
restoreWindow = replaceBrowserGlobal("window", { localStorage: storage });
assert.doesNotThrow(() => setDraft("thread-1", "x"));
assert.equal(getDraft("thread-1"), "");
assert.doesNotThrow(() => clearDraft("thread-1"));

View File

@@ -33,12 +33,12 @@ function jsonResponse(body, status = 200) {
function installFetch(handler) {
const originalFetch = globalThis.fetch;
const originalSessionStorage = globalThis.sessionStorage;
const originalWindow = globalThis.window;
const calls = [];
replaceBrowserGlobal("window", { location: { origin: "http://localhost" } });
replaceBrowserGlobal(
const restoreWindow = replaceBrowserGlobal("window", {
location: { origin: "http://localhost" },
});
const restoreSessionStorage = replaceBrowserGlobal(
"sessionStorage",
createMemoryStorage({ ironclaw_token: "token-1" }),
);
@@ -51,8 +51,8 @@ function installFetch(handler) {
calls,
restore() {
globalThis.fetch = originalFetch;
globalThis.sessionStorage = originalSessionStorage;
globalThis.window = originalWindow;
restoreSessionStorage();
restoreWindow();
},
};
}