// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { render, screen, act } from "@testing-library/react"; import "@testing-library/jest-dom"; import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts"; import {type cell, useCell} from "../../../src/utils/cellStore.ts"; import { StrictMode } from "react"; import { API_BASE_URL } from "../../../src/config/api.ts"; jest.mock("../../../src/utils/priorityFiltering.ts", () => ({ applyPriorityPredicates: jest.fn((_log, preds: any[]) => preds.every(() => true) // default: pass all ), })); import {applyPriorityPredicates} from "../../../src/utils/priorityFiltering.ts"; class MockEventSource { url: string; onmessage: ((event: { data: string }) => void) | null = null; onerror: ((event: unknown) => void) | null = null; close = jest.fn(); constructor(url: string) { this.url = url; // expose the latest instance for tests: (globalThis as any).__es = this; } } beforeAll(() => { globalThis.EventSource = MockEventSource as any; }); afterEach(() => { // reset mock so previous instance not reused accidentally (globalThis as any).__es = undefined; jest.clearAllMocks(); }); function LogsProbe({ filters }: { filters: Map }) { const { filteredLogs, distinctNames } = useLogs(filters); return (
{distinctNames.size}
); } function LogItem({ cell: c, index }: { cell: ReturnType>; index: number }) { const value = useCell(c); return (
  • {value.name} {value.message} {String(value.firstCreated)} {String(value.created)} {value.reference ?? ""}
  • ); } function emit(log: LogRecord) { const eventSource = (globalThis as any).__es as MockEventSource; if (!eventSource || !eventSource.onmessage) throw new Error("EventSource not initialized"); act(() => { eventSource.onmessage!({ data: JSON.stringify(log) }); }); } describe("useLogs (unit)", () => { it("creates EventSource once and closes on unmount", () => { const filters = new Map(); // allow all by default const { unmount } = render( ); const es = (globalThis as any).__es as MockEventSource; expect(es).toBeTruthy(); expect(es.url).toBe(`${API_BASE_URL}/logs/stream`); unmount(); expect(es.close).toHaveBeenCalledTimes(1); }); it("appends filtered logs and collects distinct names", () => { const filters = new Map(); render( ); expect(screen.getByTestId("names-count")).toHaveTextContent("0"); emit({ levelname: "DEBUG", levelno: 10, name: "alpha", message: "m1", created: 1, relativeCreated: 1, firstCreated: 1, firstRelativeCreated: 1, }); emit({ levelname: "DEBUG", levelno: 10, name: "beta", message: "m2", created: 2, relativeCreated: 2, firstCreated: 2, firstRelativeCreated: 2, }); emit({ levelname: "DEBUG", levelno: 10, name: "alpha", message: "m3", created: 3, relativeCreated: 3, firstCreated: 3, firstRelativeCreated: 3, }); // 3 messages (no reference), 2 distinct names expect(screen.getAllByRole("listitem")).toHaveLength(3); expect(screen.getByTestId("names-count")).toHaveTextContent("2"); expect(screen.getByTestId("log-0-name")).toHaveTextContent("alpha"); expect(screen.getByTestId("log-1-name")).toHaveTextContent("beta"); expect(screen.getByTestId("log-2-name")).toHaveTextContent("alpha"); }); it("updates first message with reference when a second one with that reference comes", () => { const filters = new Map(); render(); // First message with ref r1 emit({ levelname: "DEBUG", levelno: 10, name: "svc", message: "first", reference: "r1", created: 10, relativeCreated: 10, firstCreated: 10, firstRelativeCreated: 10, }); // Second message with same ref r1, should still be a single item emit({ levelname: "DEBUG", levelno: 10, name: "svc", message: "second", reference: "r1", created: 20, relativeCreated: 20, firstCreated: 20, firstRelativeCreated: 20, }); const items = screen.getAllByRole("listitem"); expect(items).toHaveLength(1); // Same single item, but message should be "second" expect(screen.getByTestId("log-0-msg")).toHaveTextContent("second"); // The "firstCreated" should remain the original (10), while "created" is now 20 expect(screen.getByTestId("log-0-first")).toHaveTextContent("10"); expect(screen.getByTestId("log-0-created")).toHaveTextContent("20"); expect(screen.getByTestId("log-0-ref")).toHaveTextContent("r1"); }); it("runs recomputeFiltered when filters change", () => { const allowAll = new Map(); const { rerender } = render(); emit({ levelname: "DEBUG", levelno: 10, name: "n1", message: "ok", created: 1, relativeCreated: 1, firstCreated: 1, firstRelativeCreated: 1, }); emit({ levelname: "DEBUG", levelno: 10, name: "n2", message: "ok", created: 2, relativeCreated: 2, firstCreated: 2, firstRelativeCreated: 2, }); emit({ levelname: "INFO", levelno: 20, name: "n3", message: "ok1", reference: "r1", created: 3, relativeCreated: 3, firstCreated: 3, firstRelativeCreated: 3, }); emit({ levelname: "INFO", levelno: 20, name: "n3", message: "ok2", reference: "r1", created: 4, relativeCreated: 4, firstCreated: 4, firstRelativeCreated: 4, }); expect(screen.getAllByRole("listitem")).toHaveLength(3); // Now change filters to block all < INFO (applyPriorityPredicates as jest.Mock).mockImplementation((l) => l.levelno >= 20); const blockDebug = new Map([["dummy", { value: true }]]); rerender(); // Should recompute with shorter list expect(screen.queryAllByRole("listitem")).toHaveLength(1); // Switch back to allow-all (applyPriorityPredicates as jest.Mock).mockImplementation((_log, preds: any[]) => preds.every(() => true) ); rerender(); // recompute should restore all three expect(screen.getAllByRole("listitem")).toHaveLength(3); }); });