diff --git a/web/src/components/delete-confirm-dialog.test.tsx b/web/src/components/delete-confirm-dialog.test.tsx
new file mode 100644
index 0000000..9374ea5
--- /dev/null
+++ b/web/src/components/delete-confirm-dialog.test.tsx
@@ -0,0 +1,35 @@
+import { expect, test, vi } from "vitest";
+import { screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import { renderApp } from "../test/render";
+import { DeleteConfirmDialog } from "./delete-confirm-dialog";
+import { InUseError } from "../api/errors";
+
+test("delete-in-use shows the in-use count and keeps the dialog open", async () => {
+ const onConfirm = vi.fn(() => Promise.reject(new InUseError(3)));
+ renderApp();
+
+ await userEvent.click(screen.getByRole("button", { name: /delete/i }));
+
+ const dialog = within(document.body);
+ const buttons = await dialog.findAllByRole("button", { name: /delete/i });
+ await userEvent.click(buttons[buttons.length - 1]);
+
+ expect(await dialog.findByText(/used by 3/i)).toBeInTheDocument();
+ expect(dialog.getByText("Delete this term?")).toBeInTheDocument();
+});
+
+test("a clean confirm closes the dialog", async () => {
+ const onConfirm = vi.fn(() => Promise.resolve());
+ renderApp();
+
+ await userEvent.click(screen.getByRole("button", { name: /delete/i }));
+
+ const dialog = within(document.body);
+ const buttons = await dialog.findAllByRole("button", { name: /delete/i });
+ await userEvent.click(buttons[buttons.length - 1]);
+
+ await waitFor(() => expect(dialog.queryByText("Delete this term?")).toBeNull());
+ expect(onConfirm).toHaveBeenCalledTimes(1);
+});
diff --git a/web/src/lib/format-date.test.ts b/web/src/lib/format-date.test.ts
new file mode 100644
index 0000000..f065d50
--- /dev/null
+++ b/web/src/lib/format-date.test.ts
@@ -0,0 +1,19 @@
+import { expect, test } from "vitest";
+
+import { formatDate } from "./format-date";
+
+test("formats a date-only string in the locale without a timezone day-shift", () => {
+ expect(formatDate("1962-04-03", "en")).toBe("Apr 3, 1962");
+});
+
+test("returns the em-dash placeholder for null", () => {
+ expect(formatDate(null, "en")).toBe("—");
+});
+
+test("returns an unparseable string unchanged", () => {
+ expect(formatDate("not-a-date", "en")).toBe("not-a-date");
+});
+
+test("stringifies a non-string, non-null value", () => {
+ expect(formatDate(42, "en")).toBe("42");
+});
diff --git a/web/src/lib/labels.test.ts b/web/src/lib/labels.test.ts
new file mode 100644
index 0000000..af609f9
--- /dev/null
+++ b/web/src/lib/labels.test.ts
@@ -0,0 +1,24 @@
+import { expect, test } from "vitest";
+
+import { labelText } from "./labels";
+
+const labels = [
+ { lang: "en", label: "Bowl" },
+ { lang: "sv", label: "Skål" },
+];
+
+test("returns the exact-language label when present", () => {
+ expect(labelText(labels, "sv")).toBe("Skål");
+});
+
+test("falls back to the English label when the requested language is missing", () => {
+ expect(labelText(labels, "de")).toBe("Bowl");
+});
+
+test("falls back to the first label when neither the language nor English is present", () => {
+ expect(labelText([{ lang: "fr", label: "Bol" }], "de")).toBe("Bol");
+});
+
+test("returns an empty string for no labels", () => {
+ expect(labelText([], "en")).toBe("");
+});
diff --git a/web/src/objects/prune-fields.test.ts b/web/src/objects/prune-fields.test.ts
new file mode 100644
index 0000000..fd9ff3a
--- /dev/null
+++ b/web/src/objects/prune-fields.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from "vitest";
+
+import { pruneFields } from "./prune-fields";
+
+test("drops empty/null/undefined scalars, keeps real scalars", () => {
+ const out = pruneFields(
+ { a: "x", b: "", c: null, d: undefined, e: 0, f: false },
+ new Set(),
+ "en",
+ );
+ expect(out).toEqual({ a: "x", e: 0, f: false });
+});
+
+test("a localized_text key keeps only the default-language entry", () => {
+ const out = pruneFields(
+ { title: { en: "Bowl", sv: "Skål" } },
+ new Set(["title"]),
+ "sv",
+ );
+ expect(out).toEqual({ title: { sv: "Skål" } });
+});
+
+test("a non-localized object value keeps all non-empty entries", () => {
+ const out = pruneFields(
+ { dims: { w: "10", h: "", d: "5" } },
+ new Set(),
+ "en",
+ );
+ expect(out).toEqual({ dims: { w: "10", d: "5" } });
+});
+
+test("an object value left with no entries is dropped entirely", () => {
+ const out = pruneFields(
+ { title: { en: "Bowl" }, empty: { en: "", sv: "" } },
+ new Set(["title", "empty"]),
+ "sv",
+ );
+ expect(out).toEqual({});
+});