Add experiment logs to the monitoring page

This commit is contained in:
Twirre
2026-01-28 10:15:58 +00:00
committed by Björn Otgaar
parent 78901ee25b
commit 835de03a29
25 changed files with 619 additions and 124 deletions

View File

@@ -11,8 +11,6 @@ const loggingStoreRef: { current: null | { setState: (state: Partial<LoggingSett
type LoggingSettingsState = {
showRelativeTime: boolean;
setShowRelativeTime: (show: boolean) => void;
scrollToBottom: boolean;
setScrollToBottom: (scroll: boolean) => void;
};
jest.mock("zustand", () => {
@@ -59,8 +57,8 @@ type LoggingComponent = typeof import("../../../src/components/Logging/Logging.t
let Logging: LoggingComponent;
beforeAll(async () => {
if (!Element.prototype.scrollIntoView) {
Object.defineProperty(Element.prototype, "scrollIntoView", {
if (!Element.prototype.scrollTo) {
Object.defineProperty(Element.prototype, "scrollTo", {
configurable: true,
writable: true,
value: function () {},
@@ -84,7 +82,6 @@ afterEach(() => {
function resetLoggingStore() {
loggingStoreRef.current?.setState({
showRelativeTime: false,
scrollToBottom: true,
});
}
@@ -151,7 +148,7 @@ describe("Logging component", () => {
];
mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
const user = userEvent.setup();
const view = render(<Logging/>);
@@ -175,7 +172,7 @@ describe("Logging component", () => {
const logCell = makeCell({message: "Initial", firstRelativeCreated: 42});
mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
render(<Logging/>);
await waitFor(() => {
@@ -209,7 +206,7 @@ describe("Logging component", () => {
const initialMap = firstProps.filterPredicates;
expect(initialMap).toBeInstanceOf(Map);
expect(initialMap.size).toBe(0);
expect(initialMap.size).toBe(1); // Initially, only filter out experiment logs
expect(mockUseLogs).toHaveBeenCalledWith(initialMap);
const updatedPredicate: LogFilterPredicate = {

View File

@@ -1,2 +1,46 @@
// Adds jest-dom matchers for React testing library
import '@testing-library/jest-dom';
import '@testing-library/jest-dom';
// Minimal browser API mocks for the test environment.
// Fetch
if (!globalThis.fetch) {
globalThis.fetch = jest.fn(async () => ({
ok: true,
status: 200,
json: async () => [],
text: async () => '',
})) as unknown as typeof fetch;
}
// EventSource
if (!globalThis.EventSource) {
class MockEventSource {
url: string;
readyState = 1;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
onopen: ((event: Event) => void) | null = null;
constructor(url: string) {
this.url = url;
}
close() {
this.readyState = 2;
}
addEventListener(type: string, listener: (event: MessageEvent) => void) {
if (type === 'message') {
this.onmessage = listener;
}
}
removeEventListener(type: string, listener: (event: MessageEvent) => void) {
if (type === 'message' && this.onmessage === listener) {
this.onmessage = null;
}
}
}
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
}

View File

@@ -0,0 +1,34 @@
import capitalize from "../../src/utils/capitalize.ts";
describe('capitalize', () => {
it('capitalizes the first letter of a lowercase word', () => {
expect(capitalize('hello')).toBe('Hello');
});
it('keeps the first letter capitalized if already uppercase', () => {
expect(capitalize('Hello')).toBe('Hello');
});
it('handles single character strings', () => {
expect(capitalize('a')).toBe('A');
expect(capitalize('A')).toBe('A');
});
it('returns empty string for empty input', () => {
expect(capitalize('')).toBe('');
});
it('only capitalizes the first letter, leaving the rest unchanged', () => {
expect(capitalize('hELLO')).toBe('HELLO');
expect(capitalize('hello world')).toBe('Hello world');
});
it('handles strings starting with numbers', () => {
expect(capitalize('123abc')).toBe('123abc');
});
it('handles strings starting with special characters', () => {
expect(capitalize('!hello')).toBe('!hello');
expect(capitalize(' hello')).toBe(' hello');
});
});

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import delayedResolve from "../../src/utils/delayedResolve.ts";
describe('delayedResolve', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('returns the resolved value of the promise', async () => {
const resultPromise = delayedResolve(Promise.resolve('hello'), 100);
await jest.advanceTimersByTimeAsync(100);
expect(await resultPromise).toBe('hello');
});
it('waits at least minDelayMs before resolving', async () => {
let resolved = false;
const resultPromise = delayedResolve(Promise.resolve('fast'), 100);
resultPromise.then(() => { resolved = true; });
await jest.advanceTimersByTimeAsync(50);
expect(resolved).toBe(false);
await jest.advanceTimersByTimeAsync(50);
expect(resolved).toBe(true);
});
it('resolves immediately after slow promise if it exceeds minDelayMs', async () => {
let resolved = false;
const slowPromise = new Promise<string>(resolve =>
setTimeout(() => resolve('slow'), 150)
);
const resultPromise = delayedResolve(slowPromise, 50);
resultPromise.then(() => { resolved = true; });
await jest.advanceTimersByTimeAsync(50);
expect(resolved).toBe(false);
await jest.advanceTimersByTimeAsync(100);
expect(resolved).toBe(true);
expect(await resultPromise).toBe('slow');
});
it('propagates rejections from the promise', async () => {
const error = new Error('test error');
const rejectedPromise = Promise.reject(error);
const resultPromise = delayedResolve(rejectedPromise, 100);
const assertion = expect(resultPromise).rejects.toThrow('test error');
await jest.advanceTimersByTimeAsync(100);
await assertion;
});
it('works with different value types', async () => {
const test = async <T>(value: T) => {
const resultPromise = delayedResolve(Promise.resolve(value), 10);
await jest.advanceTimersByTimeAsync(10);
return resultPromise;
};
expect(await test(42)).toBe(42);
expect(await test({ foo: 'bar' })).toEqual({ foo: 'bar' });
expect(await test([1, 2, 3])).toEqual([1, 2, 3]);
expect(await test(null)).toBeNull();
});
it('handles zero delay', async () => {
const resultPromise = delayedResolve(Promise.resolve('instant'), 0);
await jest.advanceTimersByTimeAsync(0);
expect(await resultPromise).toBe('instant');
});
});