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