diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4701f44 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests with coverage + run: pnpm test -- --watch=false --coverage + env: + CI: true + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage/ + retention-days: 30 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a5a8f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.PHONY: test test-watch test-coverage test-file lint lint-fix help + +# Run all tests once (CI mode) +test: + CI=1 npm test -- --watch=false + +# Run tests in watch mode (interactive development) +test-watch: + npm test + +# Run tests with coverage report +test-coverage: + CI=1 npm test -- --coverage --watch=false + +# Run a specific test file +# Usage: make test-file FILE=src/utils/__tests__/retry-with-fallback.test.js +test-file: + @if [ -z "$(FILE)" ]; then \ + echo "Error: FILE parameter is required"; \ + echo "Usage: make test-file FILE=src/utils/__tests__/retry-with-fallback.test.js"; \ + exit 1; \ + fi + CI=1 npm test -- --watch=false --testPathPattern="$(FILE)" + +# Run ESLint +lint: + npm run lint + +# Auto-fix ESLint issues +lint-fix: + npm run lint:fix + +# Show available commands +help: + @echo "Available make commands:" + @echo " make test - Run all tests once (CI mode)" + @echo " make test-watch - Run tests in watch mode (interactive)" + @echo " make test-coverage - Run tests with coverage report" + @echo " make test-file FILE= - Run specific test file" + @echo " make lint - Run ESLint" + @echo " make lint-fix - Auto-fix ESLint issues" + @echo " make help - Show this help message" diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..e6ed651 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,335 @@ +# Testing Guide + +## Overview + +This project uses **Jest** and **React Testing Library** for testing. Tests are located in `src/utils/__tests__/` directories alongside the code they test. + +### Test Framework Stack + +- **Jest**: Test runner and assertion library (via `react-scripts test`) +- **React Testing Library**: Component testing utilities +- **@testing-library/jest-dom**: Custom Jest matchers for DOM assertions +- **@testing-library/user-event**: User interaction simulation + +## Running Tests + +### Using npm + +```bash +# Run all tests (watch mode, interactive) +npm test + +# Run all tests once (CI mode) +npm test -- --watch=false + +# Run specific test file +npm test -- src/utils/__tests__/retry-with-fallback.test.js + +# Run tests matching a pattern +npm test -- --testNamePattern="should track provider health" + +# Run with coverage +npm test -- --coverage --watch=false +``` + +### Using Makefile + +```bash +# Run all tests once (CI mode) +make test + +# Run tests in watch mode +make test-watch + +# Run tests with coverage report +make test-coverage + +# Run specific test file +make test-file FILE=src/utils/__tests__/retry-with-fallback.test.js + +# Run linter +make lint + +# Auto-fix linting issues +make lint-fix + +# Show all available commands +make help +``` + +## Test Structure + +### Current Test Files + +#### `src/utils/__tests__/retry-with-fallback.test.js` + +Tests for resilient RPC call utilities: +- `createSearchDepthAwareRetry`: Exponential backoff retry mechanism +- `CircuitBreaker`: Circuit breaker pattern for failing providers +- `ProviderHealthMonitor`: Provider health tracking and status + +**Key test cases:** +- Retry on failure with exponential backoff +- Stop retrying when search depth is too restrictive +- Respect max attempts limit +- Circuit breaker opens after threshold failures +- Circuit breaker closes after timeout +- Track provider health (healthy/degraded/unhealthy/rate_limited) +- Detect rate limiting errors + +#### `src/utils/__tests__/settings-consistency.test.js` + +Tests for settings integration with provider management: +- Custom RPC URL handling +- Settings updates and fallback provider management +- Default vs custom RPC URL selection +- Empty and partial settings handling + +**Key test cases:** +- Use custom RPC URL from settings when enabled +- Fall back to default RPC URL when custom RPC is disabled +- Update fallback providers when settings change +- Handle empty and partial settings gracefully + +## Testing Patterns + +### Testing Utility Functions + +```javascript +import { yourUtilityFunction } from '../your-utility'; + +describe('YourUtility', () => { + it('should do something specific', () => { + const result = yourUtilityFunction(input); + expect(result).toBe(expectedValue); + }); +}); +``` + +### Testing Async Functions + +```javascript +it('should handle async operations', async () => { + const result = await asyncFunction(); + expect(result).toBe(expectedValue); +}); + +// Or with promises +it('should reject on error', async () => { + await expect(failingAsyncFunction()).rejects.toThrow('Error message'); +}); +``` + +### Testing Classes + +```javascript +import { YourClass } from '../your-class'; + +describe('YourClass', () => { + let instance; + + beforeEach(() => { + instance = new YourClass(); + }); + + it('should initialize correctly', () => { + expect(instance.property).toBeDefined(); + }); +}); +``` + +### Testing with Mocks + +```javascript +// Mock a function +const mockCallback = jest.fn(); +mockCallback.mockReturnValue(42); + +// Mock a module +jest.mock('../module', () => ({ + someFunction: jest.fn(() => 'mocked value') +})); +``` + +## Troubleshooting + +### Tests Are Stuck in Watch Mode + +Press `q` to quit watch mode, or use `CI=1` environment variable: + +```bash +CI=1 npm test -- --watch=false +``` + +Or simply use: + +```bash +make test +``` + +### Test Fails with "Cannot find module" + +Ensure the import path is correct relative to the test file. Jest uses the same module resolution as your app. + +### Test Fails with "TypeError: X is not a constructor" + +Check that you're importing the correct export type: + +```javascript +// Default export +import Something from './module'; + +// Named export +import { Something } from './module'; + +// Both +import Something, { OtherThing } from './module'; +``` + +### Tests Pass Locally but Fail in CI + +1. Check for timing issues - use `waitFor` for async operations +2. Ensure no tests depend on execution order +3. Check for environment-specific code (browser APIs, localStorage, etc.) + +### Coverage Is Lower Than Expected + +Run coverage report to see what's missing: + +```bash +make test-coverage +``` + +Coverage report will be in `coverage/lcov-report/index.html`. + +## Best Practices + +### Write Descriptive Test Names + +```javascript +// Good +it('should return empty array when no claims are found', () => { ... }); + +// Bad +it('works', () => { ... }); +``` + +### Test One Thing Per Test + +```javascript +// Good +it('should validate email format', () => { ... }); +it('should reject empty email', () => { ... }); + +// Bad +it('should validate email', () => { + // tests multiple scenarios +}); +``` + +### Use `beforeEach` for Setup + +```javascript +describe('MyClass', () => { + let instance; + + beforeEach(() => { + instance = new MyClass(); + }); + + it('test 1', () => { ... }); + it('test 2', () => { ... }); +}); +``` + +### Mock External Dependencies + +Don't make real network calls or use real Web3 providers in tests. Mock them: + +```javascript +jest.mock('../provider-manager', () => ({ + getProvider: jest.fn(() => mockProvider) +})); +``` + +### Test Error Cases + +```javascript +it('should throw error when required param is missing', () => { + expect(() => functionCall()).toThrow('Expected error message'); +}); +``` + +### Clean Up After Tests + +```javascript +afterEach(() => { + jest.clearAllMocks(); + // Clean up any side effects +}); +``` + +## Writing New Tests + +### 1. Create Test File + +Place test files next to the code they test: + +``` +src/utils/ + ├── your-utility.js + └── __tests__/ + └── your-utility.test.js +``` + +### 2. Import Dependencies + +```javascript +import { yourFunction } from '../your-utility'; +``` + +### 3. Organize Tests with `describe` + +```javascript +describe('YourUtility', () => { + describe('specificFunction', () => { + it('should handle case 1', () => { ... }); + it('should handle case 2', () => { ... }); + }); +}); +``` + +### 4. Write Assertions + +```javascript +expect(result).toBe(expected); +expect(result).toEqual(expected); // For objects/arrays +expect(result).toBeDefined(); +expect(result).toBeTruthy(); +expect(array).toContain(item); +expect(fn).toThrow(); +``` + +## CI/CD Integration + +Tests run automatically in CI with: + +```bash +CI=1 npm test -- --watch=false +``` + +The `CI=1` flag disables watch mode and runs tests once. + +## Future Improvements + +- Add component tests for React components +- Add integration tests for bridge operations +- Add E2E tests for complete user flows +- Set up test coverage thresholds +- Add pre-commit hooks to run tests + +## Resources + +- [Jest Documentation](https://jestjs.io/docs/getting-started) +- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) +- [Jest DOM Matchers](https://github.com/testing-library/jest-dom) diff --git a/src/utils/__tests__/retry-with-fallback.test.js b/src/utils/__tests__/retry-with-fallback.test.js index ee9c287..8be5b32 100644 --- a/src/utils/__tests__/retry-with-fallback.test.js +++ b/src/utils/__tests__/retry-with-fallback.test.js @@ -99,14 +99,15 @@ describe('Retry with Fallback', () => { describe('ProviderHealthMonitor', () => { it('should track provider health correctly', () => { const monitor = new ProviderHealthMonitor(); - + // Record some requests monitor.recordRequest('ETHEREUM', null, true, 100); monitor.recordRequest('ETHEREUM', null, true, 200); - monitor.recordRequest('ETHEREUM', null, false, 300, new Error('429')); - + monitor.recordRequest('ETHEREUM', null, true, 150); + monitor.recordRequest('ETHEREUM', null, false, 300, new Error('Network error')); + const health = monitor.getProviderHealth('ETHEREUM'); - expect(health).toBe('degraded'); // 2/3 success rate + expect(health).toBe('degraded'); // 3/4 = 75% success rate }); it('should detect rate limiting', () => { diff --git a/src/utils/provider-manager.js b/src/utils/provider-manager.js index 1e33ab4..74625d2 100644 --- a/src/utils/provider-manager.js +++ b/src/utils/provider-manager.js @@ -373,6 +373,9 @@ class ProviderManager { } } +// Export the class for testing +export { ProviderManager }; + // Create singleton instance const providerManager = new ProviderManager();