From 25044acd68eec41cd822e8d88931e22ab3dc045e Mon Sep 17 00:00:00 2001 From: Abdulkhalek Muhammad Date: Tue, 28 Jul 2026 15:56:38 +0300 Subject: [PATCH] fix(logging): stop the event log crashing on an empty select value The category filter rendered its "All categories" option as , which Radix Select 2.x rejects by throwing. The throw happened during render, so the whole Event Log tab failed to mount rather than degrading to a broken filter. Use the "all" sentinel already used by the moderation, rules and suggestions filters. The request still omits the category param entirely when "all" is selected, so the API contract is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../logging/components/EventLogBrowser.tsx | 23 +++++- .../features/logging/EventLogBrowser.test.tsx | 81 +++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 apps/dashboard/tests/client/features/logging/EventLogBrowser.test.tsx diff --git a/apps/dashboard/src/client/features/logging/components/EventLogBrowser.tsx b/apps/dashboard/src/client/features/logging/components/EventLogBrowser.tsx index 4aad5340..1f9a91ed 100644 --- a/apps/dashboard/src/client/features/logging/components/EventLogBrowser.tsx +++ b/apps/dashboard/src/client/features/logging/components/EventLogBrowser.tsx @@ -20,7 +20,20 @@ import { const PAGE_SIZE = 25; -const CATEGORY_VALUES = ["", "message", "member", "voice", "channel", "role", "server", "moderation"]; +// Radix Select forbids an empty-string item value, so "all" is the sentinel for +// "no category filter" — matching the convention used by the other filter pages. +const ALL_CATEGORIES = "all"; + +const CATEGORY_VALUES = [ + ALL_CATEGORIES, + "message", + "member", + "voice", + "channel", + "role", + "server", + "moderation", +]; const CATEGORY_COLORS: Record = { message: "text-blue-400", @@ -35,7 +48,7 @@ const CATEGORY_COLORS: Record = { export function EventLogBrowser() { const { t } = useTranslation(["logs", "common"]); const { guildId } = useParams({ from: "/guild/$guildId" }); - const [category, setCategory] = useState(""); + const [category, setCategory] = useState(ALL_CATEGORIES); const [targetId, setTargetId] = useState(""); const [page, setPage] = useState(1); @@ -43,7 +56,7 @@ export function EventLogBrowser() { page, limit: PAGE_SIZE, }; - if (category) filters.category = category; + if (category !== ALL_CATEGORIES) filters.category = category; if (targetId.trim()) filters.targetId = targetId.trim(); const { data, isLoading } = useLogEntries(guildId, filters); @@ -77,7 +90,9 @@ export function EventLogBrowser() { {CATEGORY_VALUES.map((val) => ( - {val === "" ? t("events.filter.allCategories") : t(`events.categories.${val}`)} + {val === ALL_CATEGORIES + ? t("events.filter.allCategories") + : t(`events.categories.${val}`)} ))} diff --git a/apps/dashboard/tests/client/features/logging/EventLogBrowser.test.tsx b/apps/dashboard/tests/client/features/logging/EventLogBrowser.test.tsx new file mode 100644 index 00000000..db0e93b4 --- /dev/null +++ b/apps/dashboard/tests/client/features/logging/EventLogBrowser.test.tsx @@ -0,0 +1,81 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeAll } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { LogFilters } from "../../../../src/client/features/logging/hooks/useLogging"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (k: string, o?: Record) => + o ? `${k}:${JSON.stringify(o)}` : k, + }), +})); + +vi.mock("@tanstack/react-router", () => ({ + useParams: () => ({ guildId: "g1" }), +})); + +const capturedFilters: LogFilters[] = []; +vi.mock("../../../../src/client/features/logging/hooks/useLogging", () => ({ + useLogEntries: (_guildId: string, filters: LogFilters) => { + capturedFilters.push(filters); + return { + data: { + entries: [ + { + id: "e1", + category: "member", + eventType: "memberJoin", + targetId: "u1", + executorId: null, + createdAt: new Date(0).toISOString(), + }, + ], + total: 1, + page: 1, + limit: 25, + }, + isLoading: false, + }; + }, +})); + +import { EventLogBrowser } from "../../../../src/client/features/logging/components/EventLogBrowser"; + +// Radix select needs these APIs, which jsdom lacks. +class ResizeObserverStub implements ResizeObserver { + observe(): void {} + unobserve(): void {} + disconnect(): void {} +} + +beforeAll(() => { + globalThis.ResizeObserver ??= ResizeObserverStub; + Element.prototype.scrollIntoView ??= function scrollIntoView(): void {}; + Element.prototype.hasPointerCapture ??= function hasPointerCapture(): boolean { + return false; + }; + Element.prototype.releasePointerCapture ??= + function releasePointerCapture(): void {}; +}); + +describe("EventLogBrowser", () => { + it("renders the browser and its category filter without throwing", () => { + render(); + expect(screen.getByText("events.title")).toBeInTheDocument(); + expect(screen.getByText("memberJoin")).toBeInTheDocument(); + }); + + it("sends no category param while the all-categories option is selected", async () => { + const user = userEvent.setup(); + capturedFilters.length = 0; + render(); + + await user.click(screen.getByRole("combobox")); + await user.click( + await screen.findByRole("option", { name: "events.filter.allCategories" }), + ); + + expect(capturedFilters.at(-1)?.category).toBeUndefined(); + }); +});