// @vitest-environment jsdom // // The per-history-entry scroll-id helpers + nav bus that back PageScroller's scroll restoration: // a fresh id per new view (so a forward navigation opens at the top), a stable id per entry (so Back // restores it), and a bus so a sub-view open that doesn't re-render the scroller still triggers it. import { describe, it, expect, beforeEach, vi } from "vitest"; import { nextNavId, ensureNavId, peekNavId, subscribeNav, notifyNav } from "./history"; beforeEach(() => { window.history.replaceState(null, ""); }); describe("nav scroll ids", () => { it("nextNavId hands out strictly increasing ids", () => { const a = nextNavId(); const b = nextNavId(); expect(b).toBeGreaterThan(a); }); it("ensureNavId assigns an id to a stateless entry, then is idempotent", () => { expect(peekNavId()).toBe(0); // no id yet const id = ensureNavId(); expect(id).toBeGreaterThan(0); expect(peekNavId()).toBe(id); expect(ensureNavId()).toBe(id); // does not reassign expect((window.history.state as { sfScroll?: number }).sfScroll).toBe(id); }); it("ensureNavId preserves other history.state keys", () => { window.history.replaceState({ sfPage: "plex" }, ""); ensureNavId(); expect((window.history.state as { sfPage?: string }).sfPage).toBe("plex"); }); it("a pushed entry keeps its own id (so Back restores the right one)", () => { const first = ensureNavId(); window.history.pushState({ sfScroll: nextNavId() }, ""); const second = peekNavId(); expect(second).not.toBe(first); window.history.back(); // jsdom doesn't truly pop synchronously, but the ids are distinct per entry — the invariant we need. expect(second).toBeGreaterThan(first); }); it("ensureNavId advances the counter past a higher persisted id (post-reload collision guard)", () => { // After a full reload the module counter resets to 0 while sfScroll ids persist in history.state. // Landing on an entry whose id is above the counter must push the counter past it, so the next // minted id can't duplicate a live back-stack entry and mis-restore its scroll. const high = 999999; window.history.replaceState({ sfScroll: high }, ""); expect(ensureNavId()).toBe(high); // returns the persisted id, doesn't reassign expect(nextNavId()).toBeGreaterThan(high); // and the next mint clears it }); it("notifyNav fires subscribers until they unsubscribe", () => { const cb = vi.fn(); const unsub = subscribeNav(cb); notifyNav(); expect(cb).toHaveBeenCalledTimes(1); notifyNav(); expect(cb).toHaveBeenCalledTimes(2); unsub(); notifyNav(); expect(cb).toHaveBeenCalledTimes(2); }); });