import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createBrowserHostHistory, createMemoryHostHistory } from "../src/browser/host-history.js"; describe("the browser host history", () => { beforeEach(() => { window.history.replaceState(null, "", "/"); }); afterEach(() => { window.history.replaceState(null, "", "/"); }); it("reads the address bar's path and moves it with real history entries", () => { const history = createBrowserHostHistory(window); const lengthBefore = window.history.length; history.push("/example"); expect(history.pathname()).toBe("/example"); expect(window.location.pathname).toBe("/example"); expect(window.history.length).toBe(lengthBefore + 1); }); it("adds no entry when the page asked for is already open", () => { const history = createBrowserHostHistory(window); history.push("/example"); const length = window.history.length; const listener = vi.fn(); history.subscribe(listener); history.push("/example"); expect(window.history.length).toBe(length); expect(listener).not.toHaveBeenCalled(); }); it("corrects the current entry in place with replace", () => { const history = createBrowserHostHistory(window); history.push("/example/"); const length = window.history.length; history.replace("/example"); expect(window.location.pathname).toBe("/example"); expect(window.history.length).toBe(length); }); it("notifies after its own changes and after the browser's Back and Forward", () => { const history = createBrowserHostHistory(window); const listener = vi.fn(); const unsubscribe = history.subscribe(listener); history.push("/a"); history.replace("/b"); window.dispatchEvent(new PopStateEvent("popstate")); expect(listener).toHaveBeenCalledTimes(3); unsubscribe(); window.dispatchEvent(new PopStateEvent("popstate")); history.push("/c"); expect(listener).toHaveBeenCalledTimes(3); }); it("listens to popstate only while something is subscribed", () => { const view = { addEventListener: vi.fn(), history: { pushState: vi.fn(), replaceState: vi.fn() }, location: { pathname: "/", search: "" }, removeEventListener: vi.fn(), }; const history = createBrowserHostHistory(view); const first = history.subscribe(() => undefined); const second = history.subscribe(() => undefined); expect(view.addEventListener).toHaveBeenCalledTimes(1); first(); expect(view.removeEventListener).not.toHaveBeenCalled(); second(); expect(view.removeEventListener).toHaveBeenCalledTimes(1); expect(view.removeEventListener.mock.calls[0]?.[1]).toBe( view.addEventListener.mock.calls[0]?.[1], ); }); it("stores no state object, so nothing a page put in history can be read back as authority", () => { const view = { addEventListener: vi.fn(), history: { pushState: vi.fn(), replaceState: vi.fn() }, location: { pathname: "/", search: "" }, removeEventListener: vi.fn(), }; const history = createBrowserHostHistory(view); history.push("/a"); history.replace("/b"); expect(view.history.pushState).toHaveBeenCalledWith(null, "", "/a"); expect(view.history.replaceState).toHaveBeenCalledWith(null, "", "/b"); }); }); describe("the query, which carries only where to return to (ADR 0063)", () => { it("reads the browser's query, and pushes an address that carries one", () => { const view = { addEventListener: vi.fn(), history: { pushState: vi.fn(), replaceState: vi.fn() }, location: { pathname: "/settings/signage", search: "?return=%2Fsignage" }, removeEventListener: vi.fn(), }; const history = createBrowserHostHistory(view); expect(history.search()).toBe("?return=%2Fsignage"); // The address already showing, query included, adds no entry; the same path with another query // is another address. history.push("/settings/signage?return=%2Fsignage"); expect(view.history.pushState).not.toHaveBeenCalled(); history.push("/settings/signage"); expect(view.history.pushState).toHaveBeenCalledWith(null, "", "/settings/signage"); }); it("keeps the query beside the path in memory, as the browser does", () => { const history = createMemoryHostHistory("/settings/signage?return=%2Fsignage%2Fmedia"); expect(history.pathname()).toBe("/settings/signage"); expect(history.search()).toBe("?return=%2Fsignage%2Fmedia"); history.push("/signage/media"); expect(history.search()).toBe(""); history.go(-1); expect(history.search()).toBe("?return=%2Fsignage%2Fmedia"); }); }); describe("the in-memory host history", () => { it("behaves like the browser's: push truncates forward entries, go steps within bounds", () => { const history = createMemoryHostHistory("/"); history.push("/a"); history.push("/b"); history.go(-1); expect(history.pathname()).toBe("/a"); history.push("/c"); expect(history.entries).toEqual(["/", "/a", "/c"]); history.go(-10); expect(history.pathname()).toBe("/"); history.go(10); expect(history.pathname()).toBe("/c"); }); });