94 lines
4.0 KiB
TypeScript
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);
|
|
});
|