Files
syndarix/frontend/tests/components/events/ConnectionStatus.test.tsx
Felipe Cardoso fcda8f0f96 feat(frontend): Implement client-side SSE handling (#35)
Implements real-time event streaming on the frontend with:

- Event types and type guards matching backend EventType enum
- Zustand-based event store with per-project buffering
- useProjectEvents hook with auto-reconnection and exponential backoff
- ConnectionStatus component showing connection state
- EventList component with expandable payloads and filtering

All 105 tests passing. Follows design system guidelines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:34:41 +01:00

193 lines
6.3 KiB
TypeScript

/**
* Tests for ConnectionStatus Component
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { ConnectionStatus } from '@/components/events/ConnectionStatus';
import type { SSEError } from '@/lib/types/events';
describe('ConnectionStatus', () => {
const mockOnReconnect = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('connected state', () => {
it('renders connected status', () => {
render(<ConnectionStatus state="connected" />);
expect(screen.getByText('Connected')).toBeInTheDocument();
expect(screen.getByText('Receiving real-time updates')).toBeInTheDocument();
});
it('does not show reconnect button when connected', () => {
render(<ConnectionStatus state="connected" onReconnect={mockOnReconnect} />);
expect(screen.queryByRole('button', { name: /reconnect/i })).not.toBeInTheDocument();
});
it('applies connected styling', () => {
const { container } = render(<ConnectionStatus state="connected" />);
expect(container.querySelector('.border-green-200')).toBeInTheDocument();
});
});
describe('connecting state', () => {
it('renders connecting status', () => {
render(<ConnectionStatus state="connecting" />);
expect(screen.getByText('Connecting')).toBeInTheDocument();
expect(screen.getByText('Establishing connection...')).toBeInTheDocument();
});
it('shows retry count when retrying', () => {
render(<ConnectionStatus state="connecting" retryCount={3} />);
expect(screen.getByText('Retry 3')).toBeInTheDocument();
});
});
describe('disconnected state', () => {
it('renders disconnected status', () => {
render(<ConnectionStatus state="disconnected" />);
expect(screen.getByText('Disconnected')).toBeInTheDocument();
expect(screen.getByText('Not connected to server')).toBeInTheDocument();
});
it('shows reconnect button when disconnected', () => {
render(<ConnectionStatus state="disconnected" onReconnect={mockOnReconnect} />);
const button = screen.getByRole('button', { name: /reconnect/i });
expect(button).toBeInTheDocument();
});
it('calls onReconnect when button is clicked', () => {
render(<ConnectionStatus state="disconnected" onReconnect={mockOnReconnect} />);
const button = screen.getByRole('button', { name: /reconnect/i });
fireEvent.click(button);
expect(mockOnReconnect).toHaveBeenCalledTimes(1);
});
});
describe('error state', () => {
it('renders error status', () => {
render(<ConnectionStatus state="error" />);
expect(screen.getByText('Connection Error')).toBeInTheDocument();
expect(screen.getByText('Failed to connect')).toBeInTheDocument();
});
it('shows reconnect button when in error state', () => {
render(<ConnectionStatus state="error" onReconnect={mockOnReconnect} />);
const button = screen.getByRole('button', { name: /reconnect/i });
expect(button).toBeInTheDocument();
});
it('applies error styling', () => {
const { container } = render(<ConnectionStatus state="error" />);
expect(container.querySelector('.border-destructive')).toBeInTheDocument();
});
});
describe('error details', () => {
const mockError: SSEError = {
message: 'Connection timeout',
code: 'TIMEOUT',
timestamp: '2024-01-15T10:30:00Z',
retryAttempt: 2,
};
it('shows error message when error is provided', () => {
render(<ConnectionStatus state="error" error={mockError} />);
expect(screen.getByText(/Error: Connection timeout/)).toBeInTheDocument();
});
it('shows error code when provided', () => {
render(<ConnectionStatus state="error" error={mockError} />);
expect(screen.getByText(/Code: TIMEOUT/)).toBeInTheDocument();
});
it('hides error details when showErrorDetails is false', () => {
render(<ConnectionStatus state="error" error={mockError} showErrorDetails={false} />);
expect(screen.queryByText(/Error: Connection timeout/)).not.toBeInTheDocument();
});
});
describe('compact mode', () => {
it('renders compact version', () => {
const { container } = render(<ConnectionStatus state="connected" compact />);
// Compact mode should not have the full description
expect(screen.queryByText('Receiving real-time updates')).not.toBeInTheDocument();
// Should still show the label
expect(screen.getByText('Connected')).toBeInTheDocument();
// Should use smaller container
expect(container.querySelector('.rounded-lg')).not.toBeInTheDocument();
});
it('shows compact reconnect button when disconnected', () => {
render(<ConnectionStatus state="disconnected" onReconnect={mockOnReconnect} compact />);
// Should have a small reconnect button
const button = screen.getByRole('button', { name: /reconnect/i });
expect(button).toBeInTheDocument();
expect(button.className).toContain('h-6');
});
it('shows retry count in compact mode', () => {
render(<ConnectionStatus state="connecting" retryCount={5} compact />);
expect(screen.getByText(/retry 5/i)).toBeInTheDocument();
});
});
describe('showReconnectButton prop', () => {
it('hides reconnect button when showReconnectButton is false', () => {
render(
<ConnectionStatus
state="disconnected"
onReconnect={mockOnReconnect}
showReconnectButton={false}
/>
);
expect(screen.queryByRole('button', { name: /reconnect/i })).not.toBeInTheDocument();
});
});
describe('accessibility', () => {
it('has role="status" for screen readers', () => {
render(<ConnectionStatus state="connected" />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('has aria-live="polite" for status updates', () => {
render(<ConnectionStatus state="connected" />);
const status = screen.getByRole('status');
expect(status).toHaveAttribute('aria-live', 'polite');
});
});
describe('className prop', () => {
it('applies custom className', () => {
const { container } = render(
<ConnectionStatus state="connected" className="custom-class" />
);
expect(container.querySelector('.custom-class')).toBeInTheDocument();
});
});
});