diff --git a/web/src/objects/objects-table.test.tsx b/web/src/objects/objects-table.test.tsx
index 1f3ea6e..0565ab0 100644
--- a/web/src/objects/objects-table.test.tsx
+++ b/web/src/objects/objects-table.test.tsx
@@ -1,8 +1,8 @@
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 { delay, http, HttpResponse } from "msw";
+import { Outlet, Route, Routes } from "react-router-dom";
import { server } from "../test/server";
import { renderApp } from "../test/render";
@@ -24,6 +24,24 @@ function tree() {
);
}
+function nestedTree() {
+ return (
+
+
+
+
+ >
+ }
+ >
+ detail pane} />
+
+
+ );
+}
+
/** Capture the query string of every objects request so assertions can inspect URL → request flow. */
function captureRequests() {
const calls: URLSearchParams[] = [];
@@ -134,3 +152,36 @@ test("clicking a row deep-links to /objects/:id preserving the query string", as
expect(await screen.findByRole("heading", { name: "Amphora" })).toBeInTheDocument();
});
+
+test("the object number cell is a real link", async () => {
+ renderApp(tree(), { route: "/objects" });
+ expect(await screen.findByRole("link", { name: "LM-0042" })).toBeInTheDocument();
+});
+
+test("the selected row's link is marked aria-current=page", async () => {
+ const first = objectsPage.items[0];
+ renderApp(nestedTree(), { route: `/objects/${first.id}` });
+ const link = await screen.findByRole("link", { name: first.object_number });
+ expect(link).toHaveAttribute("aria-current", "page");
+ const other = await screen.findByRole("link", { name: objectsPage.items[1].object_number });
+ expect(other).not.toHaveAttribute("aria-current");
+});
+
+test("the table is marked aria-busy while loading", async () => {
+ server.use(
+ http.get("/api/admin/objects", async () => {
+ await delay(50);
+ return HttpResponse.json(objectsPage);
+ }),
+ );
+ renderApp(tree(), { route: "/objects" });
+ expect(screen.getByRole("table")).toHaveAttribute("aria-busy", "true");
+ await screen.findByRole("link", { name: "LM-0042" });
+ expect(screen.getByRole("table")).not.toHaveAttribute("aria-busy");
+});
+
+test("a failed objects fetch is announced via role=alert", async () => {
+ server.use(http.get("/api/admin/objects", () => new HttpResponse(null, { status: 500 })));
+ renderApp(tree(), { route: "/objects" });
+ expect(await screen.findByRole("alert")).toHaveTextContent(/could not load/i);
+});
diff --git a/web/src/objects/objects-table.tsx b/web/src/objects/objects-table.tsx
index 619f671..cc67458 100644
--- a/web/src/objects/objects-table.tsx
+++ b/web/src/objects/objects-table.tsx
@@ -6,6 +6,7 @@ import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react";
import type { components } from "../api/schema";
import { useObjectsPage } from "../api/queries";
import { useDebouncedValue } from "../lib/use-debounced-value";
+import { focusRing } from "../lib/focus-ring";
import { useConfig } from "../config/config-context";
import { VisibilityBadge } from "./visibility-badge";
import { Button, buttonVariants } from "@/components/ui/button";
@@ -170,7 +171,7 @@ export function ObjectsTable() {
type="button"
aria-pressed={active}
onClick={() => setVisibility(value)}
- className={`rounded-md px-2 py-1 ${active ? "bg-primary text-primary-foreground" : "border"}`}
+ className={`${focusRing} rounded-md px-2 py-1 ${active ? "bg-primary text-primary-foreground" : "border"}`}
>
{value === "all" ? t("search.all") : t(`visibility.${value}`)}
@@ -220,7 +221,7 @@ export function ObjectsTable() {
body = (
- |
+ |
{t("objects.loadError")}
|
@@ -246,18 +247,21 @@ export function ObjectsTable() {
return (
navigate(`/objects/${object.id}?${params}`)}
- onKeyDown={(event) => {
- if (event.key === "Enter") navigate(`/objects/${object.id}?${params}`);
- }}
className={`cursor-pointer border-b text-sm ${
selected ? "bg-primary/10" : "hover:bg-muted"
}`}
>
- | {object.object_number} |
+
+ event.stopPropagation()}
+ className={`${focusRing} rounded-sm hover:underline`}
+ >
+ {object.object_number}
+
+ |
{object.object_name} |
@@ -276,7 +280,10 @@ export function ObjectsTable() {
{toolbar}
-
+
+
+ {isLoading ? t("common.loading") : t("objects.tableLabel")}
+
{columns}
{body}
|