fix(web): return a cleanup from the preload-error installer

Review feedback: window is shared across vitest tests, so repeated
installs accumulated listeners. The installer now returns a remover and
the tests detach in afterEach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rxwatcher
2026-07-22 13:43:57 +02:00
co-authored by Claude Fable 5
parent 8dc2dcbc13
commit bd97f3c123
2 changed files with 19 additions and 9 deletions
+13 -6
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { installPreloadErrorReload } from "./reloadOnPreloadError";
function harness(now: () => number) {
@@ -16,9 +16,16 @@ function harness(now: () => number) {
}
describe("installPreloadErrorReload", () => {
let cleanups: Array<() => void> = [];
afterEach(() => {
cleanups.forEach((cleanup) => cleanup());
cleanups = [];
});
it("reloads when a dynamically imported chunk fails to load", () => {
const { reload, deps } = harness(() => 1_000_000);
installPreloadErrorReload(deps);
cleanups.push(installPreloadErrorReload(deps));
window.dispatchEvent(new Event("vite:preloadError"));
expect(reload).toHaveBeenCalledTimes(1);
@@ -28,14 +35,14 @@ describe("installPreloadErrorReload", () => {
let clock = 1_000_000;
const { reload, deps } = harness(() => clock);
installPreloadErrorReload(deps);
cleanups.push(installPreloadErrorReload(deps));
window.dispatchEvent(new Event("vite:preloadError"));
expect(reload).toHaveBeenCalledTimes(1);
// Post-reload page still hits the error seconds later: give up instead
// of reload-looping.
clock += 5_000;
installPreloadErrorReload(deps);
cleanups.push(installPreloadErrorReload(deps));
window.dispatchEvent(new Event("vite:preloadError"));
expect(reload).toHaveBeenCalledTimes(1);
});
@@ -44,12 +51,12 @@ describe("installPreloadErrorReload", () => {
let clock = 1_000_000;
const { reload, deps } = harness(() => clock);
installPreloadErrorReload(deps);
cleanups.push(installPreloadErrorReload(deps));
window.dispatchEvent(new Event("vite:preloadError"));
expect(reload).toHaveBeenCalledTimes(1);
clock += 10 * 60_000;
installPreloadErrorReload(deps);
cleanups.push(installPreloadErrorReload(deps));
window.dispatchEvent(new Event("vite:preloadError"));
expect(reload).toHaveBeenCalledTimes(2);
});
+6 -3
View File
@@ -15,13 +15,13 @@ interface PreloadErrorReloadDeps {
setItem: (key: string, value: string) => void;
}
export function installPreloadErrorReload(deps?: Partial<PreloadErrorReloadDeps>): void {
export function installPreloadErrorReload(deps?: Partial<PreloadErrorReloadDeps>): () => void {
const reload = deps?.reload ?? (() => window.location.reload());
const now = deps?.now ?? Date.now;
const getItem = deps?.getItem ?? ((k: string) => sessionStorage.getItem(k));
const setItem = deps?.setItem ?? ((k: string, v: string) => sessionStorage.setItem(k, v));
window.addEventListener("vite:preloadError", (event) => {
const handler = (event: Event) => {
const lastReloadAt = Number(getItem(GUARD_KEY) ?? 0);
if (now() - lastReloadAt < GUARD_WINDOW_MS) {
// We already reloaded moments ago and the chunk is still missing;
@@ -31,5 +31,8 @@ export function installPreloadErrorReload(deps?: Partial<PreloadErrorReloadDeps>
event.preventDefault();
setItem(GUARD_KEY, String(now()));
reload();
});
};
window.addEventListener("vite:preloadError", handler);
return () => window.removeEventListener("vite:preloadError", handler);
}