Files
siftlode/frontend/src/lib/history.nav.dom.test.ts
T
peter 7d2bee59a3 fix(plex): round-2 deep-review follow-ups on the review fixes
- collections: the resync-failed-after-successful-write path now returns 422
  (honest "reconcile on next sync" detail), NOT 502. The frontend treats
  502/503/504 as gateway-unreachable and raises a sticky offline banner
  (api/core.ts RETRIABLE_GATEWAY); the app answered, so a 422 action-error
  toast is correct. Removes the false-offline regression the 502 introduced,
  and — since 422 isn't retried — the create double-write vector with it.
- tests: make the mutate-then-resync rollback assertion non-vacuous — the
  resync stub now flushes a partial local change before raising, so the test
  proves db.rollback() actually undoes it; split into a write-failed (422)
  case and a resync-failed (422 + reconcile detail + rollback) case.
- New-collection dup hint is now muted-tone with its own nameMaybeTaken copy
  (per-library uniqueness means the name may be free here) instead of the red
  authoritative nameTaken, which the still-enabled create flow contradicts.
- history: cover the navCounter post-reload collision guard (ensureNavId
  advances the counter past a higher persisted sfScroll id).
2026-08-23 07:22:53 +02:00

67 lines
2.7 KiB
TypeScript

// @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);
});
});