forked from cardosofelipe/fast-next-template
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>
193 lines
6.3 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|