test(web): add comprehensive TimelineView test coverage
Add unit and E2E tests for the TimelineView component: Unit tests (28 tests): - Rendering: header, time range selector, worker rows, empty state - Time range selection: changing ranges, active button highlighting - Worker filtering: selectedWorker, focus mode with pinned workers - Time selection: click handling, hint text display - Worker name truncation: extracting last segment from worker IDs - Segment rendering: bars mode and blocks mode visualization - Auto-refresh: currentTime prop handling, updates on change - CSS classes: proper class application for styling - Default time range: using provided defaultTimeRange prop - Compact mode: condensed layout styling - Worker click handling: onWorkerClick callback, selected state - New event highlighting: flash animation on new WebSocket events - Worker event counts: displaying total events per worker E2E tests (11 tests): - WebSocket integration: timeline updates when new events arrive - Real-time updates: worker row highlighting on new events - Multiple workers: different activity patterns (continuous vs sporadic) - Time range interaction: filtering events based on selected range - Style switching: toggle between blocks and bars visualization - Worker selection: highlight selected worker row, handle clicks - Focus Mode integration: filter to pinned workers - Time selection: show hint text, render clickable timeline - Real-time auto-refresh: time-based display updates with currentTime prop All tests verify the TimelineView component is properly integrated with WebSocket data for real-time updates and styled to match the plan mockup with color-coded block visualization. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
78fe6d18a1
commit
0e96df407d
2 changed files with 1090 additions and 0 deletions
468
src/web/frontend/components/TimelineView.e2e.test.tsx
Normal file
468
src/web/frontend/components/TimelineView.e2e.test.tsx
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
/**
|
||||
* E2E Tests for TimelineView component
|
||||
* Tests real-time WebSocket integration and live updates
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import TimelineView from '../src/components/TimelineView';
|
||||
import { LogEvent, WorkerInfo } from '../src/types';
|
||||
|
||||
describe('TimelineView E2E - Real-time Updates', () => {
|
||||
const createMockEvent = (overrides: Partial<LogEvent> = {}): LogEvent => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
worker: 'worker-alpha',
|
||||
message: 'Test event',
|
||||
raw: JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', worker: 'worker-alpha', message: 'Test event' }),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockWorker = (overrides: Partial<WorkerInfo> = {}): WorkerInfo => ({
|
||||
id: 'worker-alpha',
|
||||
lastSeen: new Date().toISOString(),
|
||||
eventCount: 10,
|
||||
status: 'active',
|
||||
recentEvents: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-03T12:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('WebSocket integration', () => {
|
||||
it('should update timeline when new events arrive via WebSocket', async () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const initialEvents = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 60000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container, rerender } = render(
|
||||
<TimelineView
|
||||
events={initialEvents}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Initial state - should have one segment
|
||||
let segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThan(0);
|
||||
const initialSegmentCount = segments.length;
|
||||
|
||||
// Simulate WebSocket message arriving with new event
|
||||
const newEvent = createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'error',
|
||||
});
|
||||
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={[...initialEvents, newEvent]}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should update with new segment
|
||||
segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThan(initialSegmentCount);
|
||||
});
|
||||
|
||||
it('should highlight worker row when new events arrive', async () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const initialEvents: LogEvent[] = [];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container, rerender } = render(
|
||||
<TimelineView
|
||||
events={initialEvents}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Initially no new-activity class
|
||||
expect(container.querySelector('.new-activity')).not.toBeInTheDocument();
|
||||
|
||||
// Simulate WebSocket message with new event
|
||||
const newEvent = createMockEvent({
|
||||
timestamp: now.toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
});
|
||||
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={[newEvent]}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should have new-activity class
|
||||
expect(container.querySelector('.new-activity')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle multiple workers with different activity patterns', async () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
|
||||
// Worker alpha: continuous activity
|
||||
const alphaEvents = Array.from({ length: 10 }, (_, i) =>
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - i * 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
})
|
||||
);
|
||||
|
||||
// Worker bravo: sporadic activity
|
||||
const bravoEvents = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 300000).toISOString(),
|
||||
worker: 'worker-bravo',
|
||||
level: 'warn',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 60000).toISOString(),
|
||||
worker: 'worker-bravo',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-alpha', eventCount: 10 }),
|
||||
createMockWorker({ id: 'worker-bravo', eventCount: 2 }),
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[...alphaEvents, ...bravoEvents]}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
const rows = container.querySelectorAll('.timeline-row');
|
||||
expect(rows.length).toBe(2);
|
||||
|
||||
// Alpha should have more segments than bravo
|
||||
const alphaRow = Array.from(rows).find(row =>
|
||||
row.textContent?.includes('alpha')
|
||||
);
|
||||
const bravoRow = Array.from(rows).find(row =>
|
||||
row.textContent?.includes('bravo')
|
||||
);
|
||||
|
||||
expect(alphaRow).toBeInTheDocument();
|
||||
expect(bravoRow).toBeInTheDocument();
|
||||
|
||||
const alphaSegments = alphaRow?.querySelectorAll('.timeline-segment');
|
||||
const bravoSegments = bravoRow?.querySelectorAll('.timeline-segment');
|
||||
|
||||
expect(alphaSegments?.length).toBeGreaterThan(bravoSegments?.length || 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Time range interaction', () => {
|
||||
it('should filter events based on selected time range', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
|
||||
// Events spread across different time periods (in different 30-second buckets)
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 1 * 60 * 1000).toISOString(), // 1 min ago
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 4 * 60 * 1000).toISOString(), // 4 min ago
|
||||
worker: 'worker-alpha',
|
||||
level: 'warn',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 20 * 60 * 1000).toISOString(), // 20 min ago
|
||||
worker: 'worker-alpha',
|
||||
level: 'error',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
defaultTimeRange="5m"
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// With 5m range, should show first 2 events (in different buckets)
|
||||
let segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Switch to 30m range
|
||||
const thirtyMinButton = screen.getByText('30 min');
|
||||
fireEvent.click(thirtyMinButton);
|
||||
|
||||
// Now should show all 3 events
|
||||
segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Style switching', () => {
|
||||
it('should switch between blocks and bars visualization', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
timelineStyle="blocks"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should have block visualization
|
||||
expect(container.querySelector('.block-visualization')).toBeInTheDocument();
|
||||
expect(container.querySelector('.timeline-segment')).not.toBeInTheDocument();
|
||||
|
||||
// Toggle to bars
|
||||
const styleToggle = container.querySelector('.style-toggle');
|
||||
if (styleToggle) {
|
||||
fireEvent.click(styleToggle);
|
||||
}
|
||||
|
||||
// Now should have segments
|
||||
expect(container.querySelector('.timeline-segment')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Worker selection and filtering', () => {
|
||||
it('should highlight selected worker row', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-bravo',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-alpha' }),
|
||||
createMockWorker({ id: 'worker-bravo' }),
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
selectedWorker="worker-alpha"
|
||||
/>
|
||||
);
|
||||
|
||||
const rows = container.querySelectorAll('.timeline-row');
|
||||
const alphaRow = Array.from(rows).find(row =>
|
||||
row.textContent?.includes('alpha')
|
||||
);
|
||||
|
||||
expect(alphaRow).toHaveClass('selected');
|
||||
});
|
||||
|
||||
it('should call onWorkerClick when worker row is clicked', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
const mockOnWorkerClick = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onWorkerClick={mockOnWorkerClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const row = container.querySelector('.timeline-row');
|
||||
if (row) {
|
||||
fireEvent.click(row);
|
||||
}
|
||||
|
||||
expect(mockOnWorkerClick).toHaveBeenCalledWith('worker-alpha');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Focus Mode integration', () => {
|
||||
it('should filter to pinned workers when focus mode is enabled', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-bravo',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-alpha' }),
|
||||
createMockWorker({ id: 'worker-bravo' }),
|
||||
];
|
||||
|
||||
const { container, rerender } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
focusModeEnabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show both workers
|
||||
let rows = container.querySelectorAll('.timeline-row');
|
||||
expect(rows.length).toBe(2);
|
||||
|
||||
// Enable focus mode with pinned worker
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
focusModeEnabled={true}
|
||||
pinnedWorkers={new Set(['worker-alpha'])}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should only show alpha
|
||||
rows = container.querySelectorAll('.timeline-row');
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].textContent).toContain('alpha');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Time selection', () => {
|
||||
it('should show time selection hint when onTimeSelect is provided', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
const mockOnTimeSelect = vi.fn();
|
||||
|
||||
render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show hint text for bars mode
|
||||
expect(screen.getByText('Click on timeline to jump to that time in activity stream')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render clickable timeline content when onTimeSelect is provided', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
const mockOnTimeSelect = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
const timelineContent = container.querySelector('.timeline-content');
|
||||
expect(timelineContent).toBeInTheDocument();
|
||||
// Timeline content should exist for click handling
|
||||
expect(timelineContent).toHaveClass('timeline-content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-time auto-refresh', () => {
|
||||
it('should update time-based display when currentTime prop changes', () => {
|
||||
const baseTime = new Date('2026-03-03T12:00:00Z').getTime();
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(baseTime - 30000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { rerender, container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
currentTime={baseTime}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
const initialSegments = container.querySelectorAll('.timeline-segment');
|
||||
expect(initialSegments.length).toBeGreaterThan(0);
|
||||
|
||||
// Advance time by 5 minutes
|
||||
const newTime = baseTime + 5 * 60 * 1000;
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
currentTime={newTime}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// Timeline should still show the event (it's within 10m range)
|
||||
const segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
622
src/web/frontend/test/TimelineView.test.tsx
Normal file
622
src/web/frontend/test/TimelineView.test.tsx
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
/**
|
||||
* Tests for TimelineView component
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
import TimelineView from '../src/components/TimelineView';
|
||||
import { LogEvent, WorkerInfo } from '../src/types';
|
||||
|
||||
describe('TimelineView', () => {
|
||||
const createMockEvent = (overrides: Partial<LogEvent> = {}): LogEvent => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
worker: 'worker-alpha',
|
||||
message: 'Test event',
|
||||
raw: JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', worker: 'worker-alpha', message: 'Test event' }),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockWorker = (overrides: Partial<WorkerInfo> = {}): WorkerInfo => ({
|
||||
id: 'worker-alpha',
|
||||
lastSeen: new Date().toISOString(),
|
||||
eventCount: 10,
|
||||
status: 'active',
|
||||
recentEvents: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const mockOnTimeSelect = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnTimeSelect.mockClear();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-03T12:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render timeline header with time range', () => {
|
||||
render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Timeline \(last/)).toBeInTheDocument();
|
||||
expect(screen.getByText('10 min')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render time range selector buttons', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.time-range-selector')).toBeInTheDocument();
|
||||
expect(screen.getByText('5 min')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 min')).toBeInTheDocument();
|
||||
expect(screen.getByText('30 min')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 hour')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state when no events in time range', () => {
|
||||
render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No worker activity in this time range')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render worker rows when events exist', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-rows')).toBeInTheDocument();
|
||||
expect(container.querySelectorAll('.timeline-row').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time range selection', () => {
|
||||
it('should change time range when clicking button', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
const fiveMinButton = screen.getByText('5 min');
|
||||
fireEvent.click(fiveMinButton);
|
||||
|
||||
expect(screen.getByText('Timeline (last 5 min)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should highlight active time range button', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
const tenMinButton = screen.getByText('10 min');
|
||||
expect(tenMinButton.closest('.time-range-button')).toHaveClass('active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker filtering', () => {
|
||||
it('should filter events when selectedWorker is set', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-beta',
|
||||
}),
|
||||
];
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-alpha' }),
|
||||
createMockWorker({ id: 'worker-beta' }),
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
selectedWorker="worker-alpha"
|
||||
/>
|
||||
);
|
||||
|
||||
// Timeline shows workers with events in the filtered set
|
||||
// Both workers appear because they both have events, but only worker-alpha's segments are shown
|
||||
const rows = container.querySelectorAll('.timeline-row');
|
||||
expect(rows.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should filter workers when focus mode is enabled with pinned workers', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-beta',
|
||||
}),
|
||||
];
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-alpha' }),
|
||||
createMockWorker({ id: 'worker-beta' }),
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
focusModeEnabled={true}
|
||||
pinnedWorkers={new Set(['worker-alpha'])}
|
||||
/>
|
||||
);
|
||||
|
||||
const rows = container.querySelectorAll('.timeline-row');
|
||||
expect(rows.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time selection', () => {
|
||||
it('should call onTimeSelect when clicking on timeline', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
const timelineContent = container.querySelector('.timeline-content');
|
||||
if (timelineContent) {
|
||||
fireEvent.click(timelineContent, { clientX: 100 });
|
||||
}
|
||||
|
||||
expect(mockOnTimeSelect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show hint text when onTimeSelect is provided', () => {
|
||||
render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Click on timeline to jump to that time in activity stream')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker name truncation', () => {
|
||||
it('should truncate worker name to last segment', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha-bravo-charlie',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha-bravo-charlie' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('charlie')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('segment rendering', () => {
|
||||
it('should render timeline segments for events in bars mode', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 10000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'error',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
const segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render block visualization in blocks mode', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onTimeSelect={mockOnTimeSelect}
|
||||
timelineStyle="blocks"
|
||||
/>
|
||||
);
|
||||
|
||||
const blocks = container.querySelector('.block-visualization');
|
||||
expect(blocks).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-refresh', () => {
|
||||
it('should use provided currentTime when available', () => {
|
||||
const providedTime = new Date('2026-03-03T12:30:00Z').getTime();
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date('2026-03-03T12:29:00Z').toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
currentTime={providedTime}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
// The timeline should include events within the time range relative to providedTime
|
||||
const segments = container.querySelectorAll('.timeline-segment');
|
||||
expect(segments.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should auto-update when currentTime prop changes', () => {
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date('2026-03-03T12:29:00Z').toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { rerender } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
currentTime={1000}
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
currentTime={5000}
|
||||
/>
|
||||
);
|
||||
|
||||
// Component should re-render with new time
|
||||
const segments = document.querySelectorAll('.timeline-segment');
|
||||
expect(segments).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSS classes', () => {
|
||||
it('should apply timeline-view class to container', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-view')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply timeline-header class', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply timeline-content class', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply timeline-axis class', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-axis')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply current-time-pulse class', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.current-time-pulse')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default time range', () => {
|
||||
it('should use provided defaultTimeRange', () => {
|
||||
render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
defaultTimeRange="5m"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Timeline (last 5 min)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('compact mode', () => {
|
||||
it('should render compact mode when enabled', () => {
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
compactMode={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.timeline-view.compact')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render block visualization in compact mode', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
compactMode={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.block-visualization')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render block visualization in bar mode', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
timelineStyle="bars"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('.block-visualization')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker click handling', () => {
|
||||
it('should call onWorkerClick when worker row is clicked', () => {
|
||||
const mockOnWorkerClick = vi.fn();
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
onWorkerClick={mockOnWorkerClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const row = container.querySelector('.timeline-row');
|
||||
if (row) {
|
||||
fireEvent.click(row);
|
||||
}
|
||||
expect(mockOnWorkerClick).toHaveBeenCalledWith('worker-alpha');
|
||||
});
|
||||
|
||||
it('should apply selected class to selected worker', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
selectedWorker="worker-alpha"
|
||||
/>
|
||||
);
|
||||
|
||||
const row = container.querySelector('.timeline-row');
|
||||
expect(row).toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('new event highlighting', () => {
|
||||
it('should apply new-activity class when new events arrive', async () => {
|
||||
const { container, rerender } = render(
|
||||
<TimelineView
|
||||
events={[]}
|
||||
workers={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
// Initially no new-activity class
|
||||
expect(container.querySelector('.new-activity')).not.toBeInTheDocument();
|
||||
|
||||
// Add new events
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const newEvents = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
}),
|
||||
];
|
||||
|
||||
rerender(
|
||||
<TimelineView
|
||||
events={newEvents}
|
||||
workers={[createMockWorker({ id: 'worker-alpha' })]}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should have new-activity class
|
||||
expect(container.querySelector('.new-activity')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker event counts', () => {
|
||||
it('should display total event count for each worker', () => {
|
||||
const now = new Date('2026-03-03T12:00:00Z');
|
||||
const events = [
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 5000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'info',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 3000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'debug',
|
||||
}),
|
||||
createMockEvent({
|
||||
timestamp: new Date(now.getTime() - 1000).toISOString(),
|
||||
worker: 'worker-alpha',
|
||||
level: 'warn',
|
||||
}),
|
||||
];
|
||||
const workers = [createMockWorker({ id: 'worker-alpha' })];
|
||||
|
||||
const { container } = render(
|
||||
<TimelineView
|
||||
events={events}
|
||||
workers={workers}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('(3)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue