import { beforeEach, expect, test } from "vitest"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { Route, Routes } from "react-router-dom"; import { server } from "../test/server"; import { renderApp } from "../test/render"; import { objectsPage } from "../test/fixtures"; import { ObjectsTable } from "./objects-table"; import { ObjectDetail } from "./object-detail"; import i18n from "../i18n"; beforeEach(async () => { await i18n.changeLanguage("en"); }); function tree() { return ( } /> } /> ); } /** Capture the query string of every objects request so assertions can inspect URL → request flow. */ function captureRequests() { const calls: URLSearchParams[] = []; server.use( http.get("/api/admin/objects", ({ request }) => { calls.push(new URL(request.url).searchParams); return HttpResponse.json(objectsPage); }), ); return calls; } function last(calls: URLSearchParams[]): URLSearchParams { return calls[calls.length - 1]; } test("renders a row per object with number, name, visibility, location and count", async () => { renderApp(tree(), { route: "/objects" }); expect(await screen.findByText("Amphora")).toBeInTheDocument(); expect(screen.getByText("LM-0042")).toBeInTheDocument(); // "Public" is also a filter chip and the location is shared across fixtures; // scope column assertions to the Amphora row. const row = screen.getByText("Amphora").closest("tr")!; expect(within(row).getByText("Public")).toBeInTheDocument(); expect(within(row).getByText("Vault 3")).toBeInTheDocument(); }); test("clicking a sortable header updates sort/order and aria-sort", async () => { const calls = captureRequests(); renderApp(tree(), { route: "/objects" }); await screen.findByText("Amphora"); const nameHeader = screen.getByRole("columnheader", { name: /Name/i }); expect(nameHeader).toHaveAttribute("aria-sort", "none"); await userEvent.click(within(nameHeader).getByRole("button")); await waitFor(() => expect(nameHeader).toHaveAttribute("aria-sort", "ascending")); await waitFor(() => { const cur = last(calls); expect(cur.get("sort")).toBe("object_name"); expect(cur.get("order")).toBe("asc"); }); await userEvent.click(within(nameHeader).getByRole("button")); await waitFor(() => expect(nameHeader).toHaveAttribute("aria-sort", "descending")); }); test("typing in the quick filter sets q (debounced)", async () => { const calls = captureRequests(); renderApp(tree(), { route: "/objects" }); await screen.findByText("Amphora"); await userEvent.type(screen.getByLabelText(/filter objects/i), "amph"); await waitFor(() => expect(last(calls).get("q")).toBe("amph")); }); test("a visibility chip sets the visibility param", async () => { const calls = captureRequests(); renderApp(tree(), { route: "/objects" }); await screen.findByText("Amphora"); await userEvent.click(screen.getByRole("button", { name: /^draft$/i })); await waitFor(() => expect(last(calls).get("visibility")).toBe("draft")); }); test("pagination next/prev change the offset", async () => { const calls = captureRequests(); // total > limit so Next is enabled. server.use( http.get("/api/admin/objects", ({ request }) => { calls.push(new URL(request.url).searchParams); return HttpResponse.json({ ...objectsPage, total: 200 }); }), ); renderApp(tree(), { route: "/objects" }); await screen.findByText("Amphora"); await userEvent.click(screen.getByRole("button", { name: /next/i })); await waitFor(() => expect(last(calls).get("offset")).toBe("50")); // Back to the first page: the URL drops `offset`, so the request sends offset 0. await userEvent.click(screen.getByRole("button", { name: /previous/i })); await waitFor(() => expect(last(calls).get("offset")).toBe("0")); }); test("the page-size select sets the limit", async () => { const calls = captureRequests(); renderApp(tree(), { route: "/objects" }); await screen.findByText("Amphora"); await userEvent.selectOptions(screen.getByLabelText(/per page/i), "100"); await waitFor(() => expect(last(calls).get("limit")).toBe("100")); }); test("clicking a row deep-links to /objects/:id preserving the query string", async () => { renderApp(tree(), { route: "/objects?sort=object_name&order=desc" }); await userEvent.click(await screen.findByText("Amphora")); expect(await screen.findByRole("heading", { name: "Amphora" })).toBeInTheDocument(); });