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:
jedarden 2026-04-27 06:36:20 -04:00
parent 78fe6d18a1
commit 0e96df407d
2 changed files with 1090 additions and 0 deletions

View 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);
});
});
});

View 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();
});
});
});