237 lines
7.3 KiB
TypeScript
237 lines
7.3 KiB
TypeScript
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);
|
|
});
|
|
});
|