import { expect, test } from "vitest";
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createMemoryRouter, Route, RouterProvider, Routes } from "react-router-dom";
import { server } from "../test/server";
import { renderApp } from "../test/render";
import { amphora } from "../test/fixtures";
import { SearchPage } from "./search-page";
import { SelectSearchPrompt } from "./select-search-prompt";
import { ObjectDetail } from "../objects/object-detail";
import "../i18n";
function tree() {
return (
}>
} />
} />
);
}
// The search rows are s. Under the shared `renderApp` harness the test
// subtree lives in a descendant under a catch-all `*` data route, where
// the data router does not intercept the link click (it falls through to a real
// browser navigation that jsdom rejects). Mounting the search routes as real
// data-router routes lets RouterProvider intercept the NavLink, which is the
// data-router equivalent of the old MemoryRouter behavior.
function renderSearchRouter(route = "/search") {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const router = createMemoryRouter(
[
{
path: "/search",
element: ,
children: [
{ index: true, element: },
{ path: ":id", element: },
],
},
],
{ initialEntries: [route] },
);
return render(
,
);
}
test("typing searches and renders highlighted rich rows", async () => {
renderApp(tree(), { route: "/search" });
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
expect(await screen.findByText("Bronze figurine")).toBeInTheDocument();
const mark = await screen.findByText("bronze");
expect(mark.tagName).toBe("MARK");
expect(screen.getByText(/25 results/i)).toBeInTheDocument();
});
test("Load more appends the next page", async () => {
renderApp(tree(), { route: "/search" });
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
await screen.findByText("Bronze figurine");
expect(screen.queryByText("Object 21")).toBeNull();
await userEvent.click(screen.getByRole("button", { name: /load more/i }));
expect(await screen.findByText("Object 21")).toBeInTheDocument();
});
test("the visibility filter adds the param to the request", async () => {
let lastVisibility: string | null = "unset";
server.use(
http.get("/api/admin/search", ({ request }) => {
lastVisibility = new URL(request.url).searchParams.get("visibility");
return HttpResponse.json({ hits: [], estimated_total: 0 });
}),
);
renderApp(tree(), { route: "/search" });
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
await userEvent.click(screen.getByRole("button", { name: /^draft$/i }));
await waitFor(() => expect(lastVisibility).toBe("draft"));
});
test("empty query shows the prompt; zero results shows empty", async () => {
renderApp(tree(), { route: "/search" });
expect(screen.getByText(/type to search/i)).toBeInTheDocument();
server.use(
http.get("/api/admin/search", () => HttpResponse.json({ hits: [], estimated_total: 0 })),
);
await userEvent.type(screen.getByLabelText(/search the collection/i), "zzz");
expect(await screen.findByText(/no results/i)).toBeInTheDocument();
});
test("clicking a result shows the object in the detail pane", async () => {
renderSearchRouter();
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
await userEvent.click(await screen.findByText("Bronze figurine"));
expect(await screen.findByText(amphora.object_name)).toBeInTheDocument();
});
test("a 503 shows the search-unavailable message", async () => {
server.use(http.get("/api/admin/search", () => new HttpResponse(null, { status: 503 })));
renderApp(tree(), { route: "/search" });
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
expect(await screen.findByText(/not available on this server/i)).toBeInTheDocument();
});
test("a 500 shows the generic search error", async () => {
server.use(http.get("/api/admin/search", () => new HttpResponse(null, { status: 500 })));
renderApp(tree(), { route: "/search" });
await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
expect(await screen.findByText(/^search is unavailable$/i)).toBeInTheDocument();
});
test("hydrates query and visibility from the initial URL", async () => {
renderApp(tree(), { route: "/search?q=bronze" });
expect(screen.getByLabelText(/search the collection/i)).toHaveValue("bronze");
expect(await screen.findByText("Bronze figurine")).toBeInTheDocument();
const { container } = renderApp(tree(), { route: "/search?q=bronze&visibility=internal" });
expect(
within(container).getByRole("button", { name: /^internal$/i }),
).toHaveAttribute("aria-pressed", "true");
});