CONTENT-WP-0002-T05 focuses search with slash, clears it with Escape, and wraps the header on a narrow screen. Assistant: grok Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { act, type ComponentProps } from "react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { mapBrowserRow, type ColumnDef } from "@/lib/browser-row";
|
|
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"],
|
|
),
|
|
];
|
|
|
|
let root: Root | null = null;
|
|
|
|
afterEach(() => {
|
|
act(() => root?.unmount());
|
|
root = null;
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
async function render(props: Partial<ComponentProps<typeof RecordBrowser>> = {}) {
|
|
const host = document.createElement("div");
|
|
document.body.appendChild(host);
|
|
root = createRoot(host);
|
|
await act(async () => {
|
|
root?.render(<RecordBrowser rows={rows} columns={columns} history={quietHistory()} {...props} />);
|
|
});
|
|
}
|
|
|
|
function quietHistory() {
|
|
return { read: () => "", push: () => undefined, subscribe: () => () => undefined };
|
|
}
|
|
|
|
describe("record browser states", () => {
|
|
it("shows loading and error without the table", async () => {
|
|
await render({ status: "loading" });
|
|
expect(document.querySelector("[role='status']")?.textContent).toBe("Loading records.");
|
|
expect(document.querySelector("table")).toBeNull();
|
|
|
|
await act(async () => root?.unmount());
|
|
await render({ status: "error", errorMessage: "The snapshot is unreadable." });
|
|
expect(document.querySelector("[role='alert']")?.textContent).toBe("The snapshot is unreadable.");
|
|
expect(document.querySelector("table")).toBeNull();
|
|
});
|
|
|
|
it("distinguishes an empty catalog from a search that matches nothing", async () => {
|
|
await render({ rows: [] });
|
|
expect(document.body.textContent).toContain("No records.");
|
|
expect(document.body.textContent).not.toContain("Clear view");
|
|
|
|
await act(async () => root?.unmount());
|
|
await render();
|
|
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, "missing");
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
input.form?.requestSubmit();
|
|
});
|
|
expect(document.body.textContent).toContain("No records match this view.");
|
|
const clear = [...document.querySelectorAll("button")].find((node) => node.textContent === "Clear view");
|
|
if (!(clear instanceof HTMLButtonElement)) throw new Error("missing clear");
|
|
await act(async () => {
|
|
clear.click();
|
|
});
|
|
expect(document.body.textContent).toContain("Alpha");
|
|
});
|
|
|
|
it("focuses search on slash and clears it on escape", async () => {
|
|
await render();
|
|
const input = document.querySelector("input[aria-label='Search records']");
|
|
if (!(input instanceof HTMLInputElement)) throw new Error("missing search");
|
|
await act(async () => {
|
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: "/", bubbles: true }));
|
|
});
|
|
expect(document.activeElement).toBe(input);
|
|
|
|
await act(async () => {
|
|
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
setValue?.call(input, "draft");
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
});
|
|
expect(input.value).toBe("");
|
|
});
|
|
});
|