ai-chatbot/tests/integration/runtime-error-recovery.test.ts

94 lines
4.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { runInNewContext } from "node:vm";
const layout = readFileSync(new URL("../../app/layout.tsx", import.meta.url), "utf8");
const script = layout.match(/const CHUNK_ERROR_RECOVERY_SCRIPT = `\\?\n([\s\S]*?)`;/)?.[1];
assert(script, "Test the exact recovery script delivered by the root layout");
function browser(pathname = "/flow-board", options: { blockedStorage?: boolean; storage?: Map<string, string> } = {}) {
const events = new Map<string, (event?: unknown) => void>();
const timers: Array<() => void> = [];
const storage = options.storage || new Map<string, string>();
const notices: unknown[] = [];
let reloads = 0;
const location = { pathname, reload: () => { reloads += 1; } };
const dom = {
body: { appendChild: (node: unknown) => notices.push(node) },
createElement: () => ({ setAttribute() {}, style: {}, textContent: "" }),
};
runInNewContext(script!, {
window: { location, addEventListener: (name: string, handler: (event?: unknown) => void) => events.set(name, handler) },
document: dom,
sessionStorage: {
getItem: (key: string) => storage.get(key),
setItem: (key: string, value: string) => { if (options.blockedStorage) throw new Error("Blocked storage"); storage.set(key, value); },
},
setTimeout: (handler: () => void) => timers.push(handler),
console: { warn() {} },
});
return {
location, storage, notices, dom,
dispatch: (name: string, event?: unknown) => events.get(name)?.(event),
flush: () => { while (timers.length) timers.shift()!(); },
reloads: () => reloads,
};
}
test("ordinary editor exceptions in bundled scripts never force a page reload", () => {
for (const path of ["/flow-board", "/"]) {
const page = browser(path);
for (let i = 0; i < 3; i += 1) {
page.dispatch("error", { message: "Cannot resolve a DOM node from Slate node", filename: "https://ai.example/_next/static/chunks/editor.js" });
page.flush();
}
assert.equal(page.reloads(), 0);
}
});
test("failed imports and rejected chunk promises never reload the flow board", () => {
const page = browser();
page.dispatch("error", { message: "ChunkLoadError: Loading chunk 123 failed" });
page.dispatch("unhandledrejection", { reason: new Error("Failed to fetch dynamically imported module") });
page.flush();
assert.equal(page.reloads(), 0);
assert.equal(page.notices.length, 1);
});
test("a genuinely missing chunk can recover a non-editor route only once", () => {
const page = browser("/");
const error = { message: "ChunkLoadError: Loading chunk 123 failed" };
page.dispatch("error", error); page.flush();
page.dispatch("error", error); page.flush();
page.dispatch("load"); page.flush();
assert.equal(page.reloads(), 1);
const afterReload = browser("/", { storage: page.storage });
afterReload.dispatch("error", error); afterReload.flush();
assert.equal(afterReload.reloads(), 0);
});
test("blocked session storage cannot create a reload loop", () => {
const page = browser("/", { blockedStorage: true });
page.dispatch("error", { message: "ChunkLoadError: Loading chunk 123 failed" });
page.flush();
assert.equal(page.reloads(), 0);
});
test("navigating into a flow board cancels an already scheduled reload", () => {
const page = browser("/");
page.dispatch("error", { message: "ChunkLoadError: Loading chunk 123 failed" });
page.location.pathname = "/flow-board";
page.flush();
assert.equal(page.reloads(), 0);
assert.equal(page.notices.length, 1);
});
test("server render failures do not masquerade as downloadable resource failures", () => {
const page = browser("/");
page.dispatch("unhandledrejection", { reason: new Error("An error occurred in the Server Components render") });
page.dispatch("error", { message: "Application error: a client-side exception has occurred", filename: "/_next/static/chunks/editor.js" });
page.flush();
assert.equal(page.reloads(), 0);
});