WellNuo/components/errors/__tests__/ErrorBoundary.test.tsx
Sergei 3260119ece Add comprehensive network error handling system
- Add networkErrorRecovery utility with:
  - Request timeout handling via AbortController
  - Circuit breaker pattern to prevent cascading failures
  - Request deduplication for concurrent identical requests
  - Enhanced fetch with timeout, circuit breaker, and retry support

- Add useApiWithErrorHandling hooks:
  - useApiCall for single API calls with auto error display
  - useMutation for mutations with optimistic update support
  - useMultipleApiCalls for parallel API execution

- Add ErrorBoundary component:
  - Catches React errors in component tree
  - Displays fallback UI with retry option
  - Supports custom fallback components
  - withErrorBoundary HOC for easy wrapping

- Add comprehensive tests (64 passing tests):
  - Circuit breaker state transitions
  - Request deduplication
  - Timeout detection
  - Error type classification
  - Hook behavior and error handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 09:29:19 -08:00

219 lines
5.8 KiB
TypeScript

/**
* Tests for ErrorBoundary component
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Text, View } from 'react-native';
import { ErrorBoundary, withErrorBoundary } from '../ErrorBoundary';
// Component that throws an error
function ThrowingComponent({ shouldThrow = true }: { shouldThrow?: boolean }) {
if (shouldThrow) {
throw new Error('Test error');
}
return <Text>Content rendered</Text>;
}
// Suppress console errors during tests
const originalConsoleError = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = originalConsoleError;
});
describe('ErrorBoundary', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render children when no error occurs', () => {
const { getByText } = render(
<ErrorBoundary>
<Text>Hello World</Text>
</ErrorBoundary>
);
expect(getByText('Hello World')).toBeTruthy();
});
it('should render fallback UI when error occurs', () => {
const { getByText } = render(
<ErrorBoundary>
<ThrowingComponent />
</ErrorBoundary>
);
expect(getByText('Oops! Something went wrong')).toBeTruthy();
expect(getByText(/The app encountered an unexpected error/)).toBeTruthy();
});
it('should render custom fallback when provided', () => {
const { getByText } = render(
<ErrorBoundary fallback={<Text>Custom Error UI</Text>}>
<ThrowingComponent />
</ErrorBoundary>
);
expect(getByText('Custom Error UI')).toBeTruthy();
});
it('should call onError callback when error occurs', () => {
const onError = jest.fn();
render(
<ErrorBoundary onError={onError}>
<ThrowingComponent />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(
expect.any(Error),
expect.objectContaining({
componentStack: expect.any(String),
})
);
});
it('should provide retry functionality', () => {
const onRetry = jest.fn();
const { getByText } = render(
<ErrorBoundary onRetry={onRetry}>
<ThrowingComponent />
</ErrorBoundary>
);
const tryAgainButton = getByText('Try Again');
fireEvent.press(tryAgainButton);
expect(onRetry).toHaveBeenCalledTimes(1);
});
it('should reset error state when retry is pressed', () => {
let shouldThrow = true;
const TestComponent = () => {
if (shouldThrow) {
throw new Error('Test');
}
return <Text>Recovered</Text>;
};
const { getByText, queryByText, rerender } = render(
<ErrorBoundary onRetry={() => { shouldThrow = false; }}>
<TestComponent />
</ErrorBoundary>
);
// Error is shown
expect(getByText('Oops! Something went wrong')).toBeTruthy();
// Press retry
fireEvent.press(getByText('Try Again'));
// Re-render to see if it recovers
rerender(
<ErrorBoundary>
<TestComponent />
</ErrorBoundary>
);
expect(getByText('Recovered')).toBeTruthy();
expect(queryByText('Oops! Something went wrong')).toBeNull();
});
it('should reset error state when resetKey changes', () => {
const { rerender, getByText, queryByText } = render(
<ErrorBoundary resetKey="key1">
<ThrowingComponent shouldThrow={true} />
</ErrorBoundary>
);
expect(getByText('Oops! Something went wrong')).toBeTruthy();
// Change resetKey and provide non-throwing child
rerender(
<ErrorBoundary resetKey="key2">
<ThrowingComponent shouldThrow={false} />
</ErrorBoundary>
);
expect(getByText('Content rendered')).toBeTruthy();
expect(queryByText('Oops! Something went wrong')).toBeNull();
});
it('should show error details in development mode', () => {
const { getByText, queryByText } = render(
<ErrorBoundary showDetails={true}>
<ThrowingComponent />
</ErrorBoundary>
);
// Details toggle should be present
expect(getByText(/Error Details/)).toBeTruthy();
// Click to expand
fireEvent.press(getByText(/Error Details/));
// Error message should be visible
expect(getByText('Test error')).toBeTruthy();
});
it('should hide error details when showDetails is false', () => {
const { queryByText } = render(
<ErrorBoundary showDetails={false}>
<ThrowingComponent />
</ErrorBoundary>
);
expect(queryByText(/Error Details/)).toBeNull();
});
});
describe('withErrorBoundary HOC', () => {
it('should wrap component with error boundary', () => {
const MyComponent = () => <Text>My Component</Text>;
const WrappedComponent = withErrorBoundary(MyComponent);
const { getByText } = render(<WrappedComponent />);
expect(getByText('My Component')).toBeTruthy();
});
it('should catch errors from wrapped component', () => {
const onError = jest.fn();
const WrappedComponent = withErrorBoundary(ThrowingComponent, { onError });
const { getByText } = render(<WrappedComponent />);
expect(getByText('Oops! Something went wrong')).toBeTruthy();
expect(onError).toHaveBeenCalled();
});
it('should pass props to wrapped component', () => {
interface Props {
message: string;
}
const MyComponent = ({ message }: Props) => <Text>{message}</Text>;
const WrappedComponent = withErrorBoundary(MyComponent);
const { getByText } = render(<WrappedComponent message="Hello Props" />);
expect(getByText('Hello Props')).toBeTruthy();
});
it('should set correct displayName', () => {
const MyComponent = () => <Text>Test</Text>;
MyComponent.displayName = 'MyComponent';
const WrappedComponent = withErrorBoundary(MyComponent);
expect(WrappedComponent.displayName).toBe('withErrorBoundary(MyComponent)');
});
});