From aef500054399c3ee4be9d28231ae859829444e87 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 9 Jun 2026 12:28:48 +0200 Subject: [PATCH] test(web): cover prune-fields, labels, format-date, delete-in-use dialog (#67) --- .../components/delete-confirm-dialog.test.tsx | 35 +++++++++++++++++ web/src/lib/format-date.test.ts | 19 +++++++++ web/src/lib/labels.test.ts | 24 ++++++++++++ web/src/objects/prune-fields.test.ts | 39 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 web/src/components/delete-confirm-dialog.test.tsx create mode 100644 web/src/lib/format-date.test.ts create mode 100644 web/src/lib/labels.test.ts create mode 100644 web/src/objects/prune-fields.test.ts 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({}); +});