CONTENT-WP-0002-T04 writes q, facet values, and sort into history so back and forward restore the table. Assistant: grok Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
168 lines
5.1 KiB
TypeScript
168 lines
5.1 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { act } from "react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { mapBrowserRow, type ColumnDef } from "@/lib/browser-row";
|
|
import { browserViewHistory, type ViewHistory } from "@/lib/table-view";
|
|
import { RecordBrowser } from "./RecordBrowser";
|
|
|
|
declare global {
|
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
}
|
|
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
const columns: ColumnDef[] = [
|
|
{ key: "name", label: "Name" },
|
|
{ key: "tldr", label: "Summary" },
|
|
];
|
|
|
|
const rows = [
|
|
mapBrowserRow(
|
|
{ id: "a", title: "Alpha", tldr: "First", tags: ["one"], kindLabel: "Concept", route: "/a/", kindHref: "/concept/" },
|
|
["tags"],
|
|
),
|
|
mapBrowserRow(
|
|
{ id: "z", title: "Zebra", tldr: "Last", tags: ["two"], kindLabel: "Concept", route: "/z/", kindHref: "/concept/" },
|
|
["tags"],
|
|
),
|
|
];
|
|
|
|
let root: Root | null = null;
|
|
|
|
afterEach(() => {
|
|
act(() => root?.unmount());
|
|
root = null;
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
function names(): string[] {
|
|
return [...document.querySelectorAll("tbody tr td:first-child")].map((cell) => cell.textContent ?? "");
|
|
}
|
|
|
|
function buttonNamed(label: string): HTMLButtonElement {
|
|
const node = [...document.querySelectorAll("button")].find((item) => item.textContent?.includes(label));
|
|
if (!(node instanceof HTMLButtonElement)) throw new Error(`missing ${label}`);
|
|
return node;
|
|
}
|
|
|
|
/** Same-document session history: back and forward keep this page and report the new query. */
|
|
function sessionHistory(): ViewHistory & { back: () => void; forward: () => void; location: () => string } {
|
|
const stack = [""];
|
|
let index = 0;
|
|
const listeners = new Set<() => void>();
|
|
const notify = () => {
|
|
for (const listener of listeners) listener();
|
|
};
|
|
return {
|
|
read: () => (stack[index] ? `?${stack[index]}` : ""),
|
|
push: (search) => {
|
|
stack.splice(index + 1);
|
|
stack.push(search);
|
|
index = stack.length - 1;
|
|
},
|
|
subscribe: (listener) => {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
back: () => {
|
|
if (index === 0) return;
|
|
index -= 1;
|
|
notify();
|
|
},
|
|
forward: () => {
|
|
if (index >= stack.length - 1) return;
|
|
index += 1;
|
|
notify();
|
|
},
|
|
location: () => stack[index],
|
|
};
|
|
}
|
|
|
|
describe("record browser history", () => {
|
|
it("reads a pushed query and a popstate traversal", () => {
|
|
window.history.replaceState(null, "", "/records/");
|
|
const history = browserViewHistory();
|
|
const seen: string[] = [];
|
|
const stop = history.subscribe(() => seen.push(history.read()));
|
|
history.push("q=zebra");
|
|
expect(window.location.search).toBe("?q=zebra");
|
|
expect(seen).toEqual([]);
|
|
window.history.pushState(null, "", "/records/?tags=one");
|
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
expect(seen).toEqual(["?tags=one"]);
|
|
stop();
|
|
});
|
|
|
|
it("restores search, facets, and sort on back and forward", async () => {
|
|
const history = sessionHistory();
|
|
const host = document.createElement("div");
|
|
document.body.appendChild(host);
|
|
root = createRoot(host);
|
|
await act(async () => {
|
|
root?.render(<RecordBrowser rows={rows} columns={columns} history={history} />);
|
|
});
|
|
expect(names()).toEqual(["Alpha", "Zebra"]);
|
|
|
|
const input = document.querySelector("input[aria-label='Search records']");
|
|
if (!(input instanceof HTMLInputElement)) throw new Error("missing search");
|
|
await act(async () => {
|
|
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
setValue?.call(input, "zebra");
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
await act(async () => {
|
|
input.form?.requestSubmit();
|
|
});
|
|
expect(history.location()).toContain("q=zebra");
|
|
expect(names()).toEqual(["Zebra"]);
|
|
|
|
await act(async () => {
|
|
buttonNamed("two").click();
|
|
});
|
|
expect(history.location()).toContain("tags=two");
|
|
expect(buttonNamed("two").getAttribute("aria-pressed")).toBe("true");
|
|
|
|
await act(async () => {
|
|
buttonNamed("Name").click();
|
|
});
|
|
expect(history.location()).toContain("sort=-name");
|
|
expect(names()).toEqual(["Zebra"]);
|
|
|
|
await act(async () => {
|
|
history.back();
|
|
});
|
|
expect(history.location()).not.toContain("sort=");
|
|
expect(history.location()).toContain("tags=two");
|
|
|
|
await act(async () => {
|
|
history.back();
|
|
});
|
|
expect(history.location()).toBe("q=zebra");
|
|
expect(buttonNamed("two").getAttribute("aria-pressed")).toBe("false");
|
|
expect(names()).toEqual(["Zebra"]);
|
|
|
|
await act(async () => {
|
|
history.back();
|
|
});
|
|
expect(history.location()).toBe("");
|
|
expect(names()).toEqual(["Alpha", "Zebra"]);
|
|
|
|
await act(async () => {
|
|
history.forward();
|
|
});
|
|
expect(history.location()).toBe("q=zebra");
|
|
expect(names()).toEqual(["Zebra"]);
|
|
|
|
await act(async () => {
|
|
history.forward();
|
|
});
|
|
expect(history.location()).toContain("tags=two");
|
|
|
|
await act(async () => {
|
|
history.forward();
|
|
});
|
|
expect(history.location()).toContain("sort=-name");
|
|
expect(names()).toEqual(["Zebra"]);
|
|
});
|
|
});
|