Files
pepperplus-ui/test/components/Logging/Logging.test.tsx
JGerla dc65b90e1c Merge branch 'dev' into chore/adding-uu-strings
# Conflicts:
#	src/components/Logging/Logging.tsx
2026-01-28 11:29:44 +01:00

240 lines
7.5 KiB
TypeScript

// 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, fireEvent, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import type {Cell} from "../../../src/utils/cellStore.ts";
import {cell} from "../../../src/utils/cellStore.ts";
import type {LogFilterPredicate, LogRecord} from "../../../src/components/Logging/useLogs.ts";
const mockFiltersRender = jest.fn();
const loggingStoreRef: { current: null | { setState: (state: Partial<LoggingSettingsState>) => void } } = { current: null };
type LoggingSettingsState = {
showRelativeTime: boolean;
setShowRelativeTime: (show: boolean) => void;
};
jest.mock("zustand", () => {
const actual = jest.requireActual("zustand");
const actualCreate = actual.create;
return {
__esModule: true,
...actual,
create: (...args: any[]) => {
const store = actualCreate(...args);
const state = store.getState();
if ("setShowRelativeTime" in state && "setScrollToBottom" in state) {
loggingStoreRef.current = store;
}
return store;
},
};
});
jest.mock("../../../src/components/Logging/Filters.tsx", () => {
const React = jest.requireActual("react");
return {
__esModule: true,
default: (props: any) => {
mockFiltersRender(props);
return React.createElement("div", {"data-testid": "filters-mock"}, "filters");
},
};
});
jest.mock("../../../src/components/Logging/useLogs.ts", () => {
const actual = jest.requireActual("../../../src/components/Logging/useLogs.ts");
return {
__esModule: true,
...actual,
useLogs: jest.fn(),
};
});
import {useLogs} from "../../../src/components/Logging/useLogs.ts";
const mockUseLogs = useLogs as jest.MockedFunction<typeof useLogs>;
type LoggingComponent = typeof import("../../../src/components/Logging/Logging.tsx").default;
let Logging: LoggingComponent;
beforeAll(async () => {
if (!Element.prototype.scrollTo) {
Object.defineProperty(Element.prototype, "scrollTo", {
configurable: true,
writable: true,
value: function () {},
});
}
({default: Logging} = await import("../../../src/components/Logging/Logging.tsx"));
});
beforeEach(() => {
mockUseLogs.mockReset();
mockFiltersRender.mockReset();
mockUseLogs.mockReturnValue({filteredLogs: [], distinctNames: new Set()});
resetLoggingStore();
});
afterEach(() => {
jest.restoreAllMocks();
});
function resetLoggingStore() {
loggingStoreRef.current?.setState({
showRelativeTime: false,
});
}
function makeRecord(overrides: Partial<LogRecord> = {}): LogRecord {
return {
name: "pepper.logger",
message: "default",
levelname: "INFO",
levelno: 20,
created: 1,
relativeCreated: 1,
firstCreated: 1,
firstRelativeCreated: 1,
...overrides,
};
}
function makeCell(overrides: Partial<LogRecord> = {}): Cell<LogRecord> {
return cell(makeRecord(overrides));
}
describe("Logging component", () => {
it("renders log messages and toggles the timestamp between absolute and relative view", async () => {
const logCell = makeCell({
name: "pepper.trace.logging",
message: "Ping",
levelname: "WARNING",
levelno: 30,
created: 1_700_000_000,
relativeCreated: 12_345,
firstCreated: 1_700_000_000,
firstRelativeCreated: 12_345,
});
const names = new Set(["pepper.trace.logging"]);
mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: names});
jest.spyOn(Date.prototype, "toLocaleTimeString").mockReturnValue("ABS TIME");
const user = userEvent.setup();
render(<Logging/>);
expect(screen.getByText("Logs")).toBeDefined();
expect(screen.getByText("WARNING")).toBeDefined();
expect(screen.getByText("logging")).toBeDefined();
expect(screen.getByText("Ping")).toBeDefined();
let timestamp = screen.queryByText("ABS TIME");
if (!timestamp) {
// if previous test left the store toggled, click once to show absolute time
timestamp = screen.getByText("00:00:12.345");
await user.click(timestamp);
timestamp = screen.getByText("ABS TIME");
}
await user.click(timestamp);
expect(screen.getByText("00:00:12.345")).toBeDefined();
});
it("shows the scroll-to-bottom button after a manual scroll and scrolls when clicked", async () => {
const logs = [
makeCell({message: "first", firstRelativeCreated: 1}),
makeCell({message: "second", firstRelativeCreated: 2}),
];
mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
const user = userEvent.setup();
const view = render(<Logging/>);
expect(screen.queryByRole("button", {name: "Scroll to bottom"})).toBeNull();
const scrollable = view.container.querySelector(".scroll-y");
expect(scrollable).toBeTruthy();
fireEvent.wheel(scrollable!);
const button = await screen.findByRole("button", {name: "Scroll to bottom"});
await user.click(button);
expect(scrollSpy).toHaveBeenCalled();
await waitFor(() => {
expect(screen.queryByRole("button", {name: "Scroll to bottom"})).toBeNull();
});
});
it("scrolls the last element into view when a log cell updates", async () => {
const logCell = makeCell({message: "Initial", firstRelativeCreated: 42});
mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
render(<Logging/>);
await waitFor(() => {
expect(scrollSpy).toHaveBeenCalledTimes(1);
});
scrollSpy.mockClear();
act(() => {
const current = logCell.get();
logCell.set({...current, message: "Updated"});
});
expect(screen.getByText("Updated")).toBeDefined();
await waitFor(() => {
expect(scrollSpy).toHaveBeenCalledTimes(1);
});
});
it("passes filter state to Filters and re-invokes useLogs when predicates change", async () => {
const distinct = new Set(["pepper.core"]);
mockUseLogs.mockImplementation((_filters: Map<string, LogFilterPredicate>) => ({
filteredLogs: [],
distinctNames: distinct,
}));
render(<Logging/>);
expect(mockFiltersRender).toHaveBeenCalledTimes(1);
const firstProps = mockFiltersRender.mock.calls[0][0];
expect(firstProps.agentNames).toBe(distinct);
const initialMap = firstProps.filterPredicates;
expect(initialMap).toBeInstanceOf(Map);
expect(initialMap.size).toBe(1); // Initially, only filter out experiment logs
expect(mockUseLogs).toHaveBeenCalledWith(initialMap);
const updatedPredicate: LogFilterPredicate = {
value: "custom",
priority: 0,
predicate: () => true,
};
act(() => {
firstProps.setFilterPredicates((prev: Map<string, LogFilterPredicate>) => {
const next = new Map(prev);
next.set("custom", updatedPredicate);
return next;
});
});
await waitFor(() => {
expect(mockUseLogs).toHaveBeenCalledTimes(2);
});
const nextFilters = mockUseLogs.mock.calls[1][0];
expect(nextFilters.get("custom")).toBe(updatedPredicate);
const secondProps = mockFiltersRender.mock.calls[mockFiltersRender.mock.calls.length - 1][0];
expect(secondProps.filterPredicates).toBe(nextFilters);
});
});