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: "/" }, 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: "/" }, 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 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"); }); });