76b2cbde1d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { http, HttpResponse } from "msw";
|
|
import { Routes, Route } from "react-router-dom";
|
|
import { server } from "../test/server";
|
|
import { renderApp } from "../test/render";
|
|
import { VocabulariesPage } from "./vocabularies-page";
|
|
import { VocabularyTerms } from "./vocabulary-terms";
|
|
import { SelectVocabularyPrompt } from "./select-vocabulary-prompt";
|
|
|
|
function tree() {
|
|
return (
|
|
<Routes>
|
|
<Route path="/vocabularies" element={<VocabulariesPage />}>
|
|
<Route index element={<SelectVocabularyPrompt />} />
|
|
<Route path=":id" element={<VocabularyTerms />} />
|
|
</Route>
|
|
</Routes>
|
|
);
|
|
}
|
|
|
|
test("lists vocabularies and creates one", async () => {
|
|
let body: unknown;
|
|
server.use(
|
|
http.post("/api/admin/vocabularies", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "v-c", key: "colour" }, { status: 201 });
|
|
}),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies" });
|
|
expect(await screen.findByText("material")).toBeInTheDocument();
|
|
await userEvent.type(screen.getByLabelText(/key/i), "colour");
|
|
await userEvent.click(screen.getByRole("button", { name: /create/i }));
|
|
await waitFor(() => expect((body as { key: string })?.key).toBe("colour"));
|
|
});
|
|
|
|
test("selecting a vocabulary shows its terms and adds one", async () => {
|
|
let termBody: unknown;
|
|
server.use(
|
|
http.post("/api/admin/vocabularies/:id/terms", async ({ request }) => {
|
|
termBody = await request.json();
|
|
return HttpResponse.json({ id: "t-c" }, { status: 201 });
|
|
}),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies/v-material" });
|
|
expect(await screen.findByText("Bronze")).toBeInTheDocument();
|
|
await userEvent.type(screen.getByLabelText(/^label$/i), "Stone");
|
|
await userEvent.click(screen.getByRole("button", { name: /add term/i }));
|
|
await waitFor(() =>
|
|
expect((termBody as { labels: { label: string }[] })?.labels[0].label).toBe("Stone"),
|
|
);
|
|
});
|
|
|
|
test("terms endpoint error shows vocab loadError", async () => {
|
|
server.use(
|
|
http.get("/api/admin/vocabularies/:id/terms", () => new HttpResponse(null, { status: 500 })),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies/v-material" });
|
|
expect(await screen.findByText(/could not load/i)).toBeInTheDocument();
|
|
});
|
|
|
|
test("add term without EN label shows required alert and does not POST", async () => {
|
|
let posted = false;
|
|
server.use(
|
|
http.post("/api/admin/vocabularies/:id/terms", () => {
|
|
posted = true;
|
|
return HttpResponse.json({ id: "t-x" }, { status: 201 });
|
|
}),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies/v-material" });
|
|
expect(await screen.findByText("Bronze")).toBeInTheDocument();
|
|
await userEvent.click(screen.getByRole("button", { name: /add term/i }));
|
|
expect(screen.getByRole("alert")).toBeInTheDocument();
|
|
expect(posted).toBe(false);
|
|
});
|
|
|
|
test("vocabularies render sorted by key", async () => {
|
|
server.use(
|
|
http.get("/api/admin/vocabularies", () =>
|
|
HttpResponse.json([
|
|
{ id: "v-zeta", key: "zeta" },
|
|
{ id: "v-alpha", key: "alpha" },
|
|
]),
|
|
),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies" });
|
|
expect(await screen.findByText("alpha")).toBeInTheDocument();
|
|
const links = screen.getAllByRole("link");
|
|
const keys = links.map((link) => link.textContent);
|
|
expect(keys.indexOf("alpha")).toBeLessThan(keys.indexOf("zeta"));
|
|
});
|
|
|
|
test("filter narrows the vocabulary list", async () => {
|
|
renderApp(tree(), { route: "/vocabularies" });
|
|
expect(await screen.findByText("material")).toBeInTheDocument();
|
|
expect(screen.getByText("technique")).toBeInTheDocument();
|
|
await userEvent.type(screen.getByRole("textbox", { name: /filter/i }), "mat");
|
|
expect(screen.getByText("material")).toBeInTheDocument();
|
|
expect(screen.queryByText("technique")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("renaming a vocabulary to an empty key does not call the rename endpoint", async () => {
|
|
let renamed = false;
|
|
server.use(
|
|
http.patch("/api/admin/vocabularies/:id", () => {
|
|
renamed = true;
|
|
return new HttpResponse(null, { status: 204 });
|
|
}),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies" });
|
|
expect(await screen.findByText("material")).toBeInTheDocument();
|
|
await userEvent.click(screen.getAllByRole("button", { name: /rename/i })[0]);
|
|
const keyInputs = screen.getAllByRole("textbox", { name: /key/i });
|
|
const input = keyInputs[keyInputs.length - 1];
|
|
await userEvent.clear(input);
|
|
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
|
expect(renamed).toBe(false);
|
|
});
|
|
|
|
test("term read row shows its external_uri as a link", async () => {
|
|
server.use(
|
|
http.get("/api/admin/vocabularies/:id/terms", () =>
|
|
HttpResponse.json([
|
|
{ id: "t-bronze", external_uri: "https://example.org/bronze", labels: [{ lang: "en", label: "Bronze" }] },
|
|
]),
|
|
),
|
|
);
|
|
renderApp(tree(), { route: "/vocabularies/v-material" });
|
|
expect(await screen.findByText("Bronze")).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: /example\.org/ })).toBeInTheDocument();
|
|
});
|