Files
ihasmail-inbuxa/web/src/lib/__tests__/staleBuild.test.ts
T
jcoffey-dev 8f9d253939 Reload even when there is an unsent draft
Holding the reload back while a compose window had unsaved text protected the
text, but it meant a tab could sit on a build the server no longer runs for as
long as someone left a draft open -- which is not automatic, and automatic is
the point.

So the reload is unconditional once the versions differ, and this will
sometimes take an unsent draft with it. The trade is deliberate: a tab talking
to a server it does not match is the worse failure, and it fails quietly.
2026-08-27 22:51:33 -07:00

127 lines
4.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { reloadIfServerRebuilt, makeConnectionWatcher, startBuildWatch } from "@/lib/staleBuild";
import { APP_VERSION } from "@/lib/version";
function healthReplies(body: unknown, ok = true) {
return vi.fn().mockResolvedValue({ ok, json: async () => body } as unknown as Response);
}
let reload: ReturnType<typeof vi.fn>;
beforeEach(() => {
sessionStorage.clear();
reload = vi.fn();
Object.defineProperty(window, "location", {
configurable: true,
value: { ...window.location, reload },
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("reloadIfServerRebuilt", () => {
it("reloads when the server reports a different build", async () => {
vi.stubGlobal("fetch", healthReplies({ ok: true, version: `${APP_VERSION}-newer` }));
expect(await reloadIfServerRebuilt()).toBe(true);
expect(reload).toHaveBeenCalledOnce();
});
it("leaves the page alone when the versions match", async () => {
vi.stubGlobal("fetch", healthReplies({ ok: true, version: APP_VERSION }));
expect(await reloadIfServerRebuilt()).toBe(false);
expect(reload).not.toHaveBeenCalled();
});
it("reloads once per version, not once per 401", async () => {
vi.stubGlobal("fetch", healthReplies({ ok: true, version: "9.9.9" }));
expect(await reloadIfServerRebuilt()).toBe(true);
expect(await reloadIfServerRebuilt()).toBe(false);
expect(reload).toHaveBeenCalledOnce();
});
it("clears the guard once the versions agree again", async () => {
vi.stubGlobal("fetch", healthReplies({ ok: true, version: "9.9.9" }));
await reloadIfServerRebuilt();
vi.stubGlobal("fetch", healthReplies({ ok: true, version: APP_VERSION }));
await reloadIfServerRebuilt();
vi.stubGlobal("fetch", healthReplies({ ok: true, version: "9.9.9" }));
expect(await reloadIfServerRebuilt()).toBe(true);
expect(reload).toHaveBeenCalledTimes(2);
});
it("does not reload when the server cannot be reached", async () => {
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("offline")));
expect(await reloadIfServerRebuilt()).toBe(false);
expect(reload).not.toHaveBeenCalled();
});
it("does not reload on a bad response or a missing version", async () => {
vi.stubGlobal("fetch", healthReplies({ ok: true, version: "9.9.9" }, false));
expect(await reloadIfServerRebuilt()).toBe(false);
vi.stubGlobal("fetch", healthReplies({ ok: true }));
expect(await reloadIfServerRebuilt()).toBe(false);
expect(reload).not.toHaveBeenCalled();
});
});
describe("noticing without being asked", () => {
it("checks when the push stream drops, but not before it has connected", async () => {
const fetchMock = healthReplies({ ok: true, version: APP_VERSION });
vi.stubGlobal("fetch", fetchMock);
const onState = makeConnectionWatcher();
// never connected: a disconnect is not news
onState("connecting");
await new Promise((r) => setTimeout(r, 0));
expect(fetchMock).not.toHaveBeenCalled();
onState("connected");
onState("connecting");
await new Promise((r) => setTimeout(r, 0));
expect(fetchMock).toHaveBeenCalled();
});
it("asks the server once when several things notice at the same moment", async () => {
const fetchMock = healthReplies({ ok: true, version: APP_VERSION });
vi.stubGlobal("fetch", fetchMock);
await Promise.all([reloadIfServerRebuilt(), reloadIfServerRebuilt(), reloadIfServerRebuilt()]);
expect(fetchMock).toHaveBeenCalledOnce();
});
});
describe("the poll is what the guarantee rests on", () => {
it("checks on its own while the tab is visible, with nobody touching it", async () => {
vi.useFakeTimers();
const fetchMock = healthReplies({ ok: true, version: "9.9.9" });
vi.stubGlobal("fetch", fetchMock);
Object.defineProperty(document, "visibilityState", { configurable: true, get: () => "visible" });
startBuildWatch();
expect(fetchMock).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(60_000);
expect(fetchMock).toHaveBeenCalled();
vi.useRealTimers();
});
it("leaves a hidden tab alone until it is looked at", async () => {
vi.useFakeTimers();
const fetchMock = healthReplies({ ok: true, version: APP_VERSION });
vi.stubGlobal("fetch", fetchMock);
let visibility = "hidden";
Object.defineProperty(document, "visibilityState", { configurable: true, get: () => visibility });
startBuildWatch();
await vi.advanceTimersByTimeAsync(180_000);
expect(fetchMock).not.toHaveBeenCalled();
visibility = "visible";
document.dispatchEvent(new Event("visibilitychange"));
await vi.advanceTimersByTimeAsync(0);
expect(fetchMock).toHaveBeenCalled();
vi.useRealTimers();
});
});