test(e4): useHistorySubview seed + popstate URL-fallback (S2 fix)
Backfills the E4 deep-review's deferred coverage for the subview history primitive. The keeper case is the S2 bookmark fix: a popstate that lands on a URL-only entry (no history.state._sub) must restore the sub-view from the ?v param via the SAME fallback the mount seed uses, not collapse to root. Covers: seed from URL vs state._sub vs root, the no-SubviewUrl state-only path, open() push+mirror + encode→null key-clear, and popstate restore from state / from URL (the fix) / collapse-to-root. 9 tests, gate green.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// useHistorySubview: a module's active sub-view rides in history.state._sub, with optional address-bar
|
||||
// mirroring so a deep sub-view (Plex show/season/info/person/playlist) is bookmarkable. This backfills
|
||||
// the E4 deep-review's deferred coverage for the S2 fix — the seed AND the popstate handler must share
|
||||
// the SAME URL fallback, so a popstate that lands on a bookmark entry (URL-only, no _sub — e.g. after
|
||||
// an overlay's programmatic history.back()) restores the sub-view from the URL instead of collapsing
|
||||
// it to root. The overlay/back-ladder half of history.ts is covered in history.dom.test.ts.
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useHistorySubview, type SubviewUrl } from "./history";
|
||||
|
||||
// A minimal string-view mirror: "root" is the un-mirrored root (encode → null), anything else mirrors
|
||||
// verbatim to ?v=<view>. decode rejects the empty string (→ null, i.e. not a valid sub-view).
|
||||
const url: SubviewUrl<string> = {
|
||||
key: "v",
|
||||
encode: (v) => (v === "root" ? null : v),
|
||||
decode: (raw) => raw || null,
|
||||
};
|
||||
|
||||
const setUrl = (search: string, state: unknown = null) =>
|
||||
window.history.replaceState(state, "", search || "/");
|
||||
const popTo = (search: string, state: unknown = null) =>
|
||||
act(() => {
|
||||
window.history.replaceState(state, "", search || "/");
|
||||
window.dispatchEvent(new PopStateEvent("popstate", { state }));
|
||||
});
|
||||
|
||||
beforeEach(() => setUrl("/"));
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
describe("useHistorySubview seed", () => {
|
||||
it("decodes the ?<key> URL param when the entry has no _sub (fresh bookmark)", () => {
|
||||
setUrl("?v=a");
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
expect(result.current.view).toBe("a");
|
||||
});
|
||||
|
||||
it("prefers history.state._sub over the URL param (in-app entry)", () => {
|
||||
setUrl("?v=a", { _sub: "b" });
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
expect(result.current.view).toBe("b");
|
||||
});
|
||||
|
||||
it("falls back to root when neither state nor a decodable URL param is present", () => {
|
||||
setUrl("/");
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
expect(result.current.view).toBe("root");
|
||||
});
|
||||
|
||||
it("ignores the URL entirely when no SubviewUrl is supplied (state-only mirror)", () => {
|
||||
setUrl("?v=a", { _sub: "fromState" });
|
||||
const { result } = renderHook(() => useHistorySubview("root"));
|
||||
expect(result.current.view).toBe("fromState");
|
||||
// A URL param with no url config must NOT leak in.
|
||||
setUrl("?v=a", null);
|
||||
const { result: r2 } = renderHook(() => useHistorySubview("root"));
|
||||
expect(r2.current.view).toBe("root");
|
||||
});
|
||||
});
|
||||
|
||||
describe("useHistorySubview open", () => {
|
||||
it("pushes state._sub and mirrors the URL via encode", () => {
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
act(() => result.current.open("a"));
|
||||
expect(result.current.view).toBe("a");
|
||||
expect(window.history.state?._sub).toBe("a");
|
||||
expect(window.location.search).toBe("?v=a");
|
||||
});
|
||||
|
||||
it("clears the mirrored key when encode returns null (root/ephemeral view)", () => {
|
||||
setUrl("?v=a");
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
act(() => result.current.open("root"));
|
||||
expect(result.current.view).toBe("root");
|
||||
expect(window.history.state?._sub).toBe("root");
|
||||
expect(window.location.search).toBe(""); // ?v dropped
|
||||
});
|
||||
});
|
||||
|
||||
describe("useHistorySubview popstate", () => {
|
||||
it("restores the sub-view from state._sub on Back/Forward", () => {
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
popTo("?v=a", { _sub: "a" });
|
||||
expect(result.current.view).toBe("a");
|
||||
});
|
||||
|
||||
it("restores from the ?<key> URL when the landed entry has no _sub — the S2 bookmark fix", () => {
|
||||
// Start in-app at root, then a popstate lands on a URL-only bookmark entry (no _sub). Reading
|
||||
// history.state alone would wrongly collapse to root; the shared URL fallback must decode ?v=b.
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
expect(result.current.view).toBe("root");
|
||||
popTo("?v=b", null);
|
||||
expect(result.current.view).toBe("b");
|
||||
});
|
||||
|
||||
it("collapses to root when a popstate lands on an entry with neither _sub nor a decodable URL", () => {
|
||||
setUrl("?v=a", { _sub: "a" });
|
||||
const { result } = renderHook(() => useHistorySubview("root", url));
|
||||
expect(result.current.view).toBe("a");
|
||||
popTo("/", null);
|
||||
expect(result.current.view).toBe("root");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user