feat: Add integration test suite for Position Manager
- Added Jest + ts-jest configuration (jest.config.js) - Added global test setup with mocks (tests/setup.ts) - Added trade factory helpers (tests/helpers/trade-factory.ts) - Added 7 test suites covering Position Manager logic: - tp1-detection.test.ts (13 tests) - breakeven-sl.test.ts (9 tests) - adx-runner-sl.test.ts (18 tests) - trailing-stop.test.ts (14 tests) - edge-cases.test.ts (18 tests) - price-verification.test.ts (13 tests) - decision-helpers.test.ts (28 tests) - Added test documentation (tests/README.md) - Updated package.json with Jest dependencies and scripts - All 113 tests pass Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
224
tests/README.md
Normal file
224
tests/README.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Position Manager Tests
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive integration test suite for the Position Manager (`lib/trading/position-manager.ts`), which manages ~1,938 lines of critical trading logic handling real capital.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode (development)
|
||||
npm run test:watch
|
||||
|
||||
# Run tests with coverage report
|
||||
npm run test:coverage
|
||||
|
||||
# Run tests for CI (with JUnit reporter)
|
||||
npm run test:ci
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── setup.ts # Global test configuration and mocks
|
||||
├── helpers/
|
||||
│ └── trade-factory.ts # Factory functions for creating mock trades
|
||||
├── integration/
|
||||
│ └── position-manager/
|
||||
│ ├── tp1-detection.test.ts # Take Profit 1 detection tests
|
||||
│ ├── breakeven-sl.test.ts # Breakeven SL after TP1 tests
|
||||
│ ├── adx-runner-sl.test.ts # ADX-based runner SL tests
|
||||
│ ├── trailing-stop.test.ts # Trailing stop functionality tests
|
||||
│ ├── edge-cases.test.ts # Edge cases and common pitfalls
|
||||
│ ├── price-verification.test.ts # Price verification before TP flags
|
||||
│ └── decision-helpers.test.ts # Decision helper function tests
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
These tests verify the **logic** of Position Manager functions in isolation:
|
||||
|
||||
- Decision helper functions (shouldStopLoss, shouldTakeProfit1, etc.)
|
||||
- Price calculation functions (calculatePrice, calculateProfitPercent)
|
||||
- ADX-based SL positioning logic
|
||||
- Trailing stop mechanics
|
||||
- Token vs USD conversion requirements
|
||||
- Price verification requirements
|
||||
|
||||
The tests extract and validate the pure calculation logic without importing the actual Position Manager, avoiding complex mocking of:
|
||||
- Drift blockchain connections
|
||||
- Pyth price feeds
|
||||
- Database operations
|
||||
- WebSocket subscriptions
|
||||
|
||||
This approach:
|
||||
1. Validates critical trading logic is correct
|
||||
2. Prevents regression of known bugs (Common Pitfalls)
|
||||
3. Enables safe refactoring of calculation functions
|
||||
4. Runs quickly without external dependencies
|
||||
|
||||
## Test Data Standards
|
||||
|
||||
All tests use standardized test data based on actual trading conditions:
|
||||
|
||||
```typescript
|
||||
TEST_DEFAULTS = {
|
||||
entry: 140.00, // Entry price
|
||||
atr: 0.43, // ATR value
|
||||
adx: 26.9, // ADX (strong trend)
|
||||
qualityScore: 95, // Signal quality
|
||||
positionSize: 8000, // Position size USD
|
||||
leverage: 15, // Leverage multiplier
|
||||
|
||||
// LONG targets
|
||||
long: {
|
||||
tp1: 141.20, // +0.86%
|
||||
tp2: 142.41, // +1.72%
|
||||
sl: 138.71, // -0.92%
|
||||
emergencySl: 137.20, // -2%
|
||||
},
|
||||
|
||||
// SHORT targets
|
||||
short: {
|
||||
tp1: 138.80, // -0.86%
|
||||
tp2: 137.59, // -1.72%
|
||||
sl: 141.29, // +0.92%
|
||||
emergencySl: 142.80, // +2%
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Common Pitfalls Covered
|
||||
|
||||
These tests specifically prevent known bugs documented in the codebase:
|
||||
|
||||
| Pitfall # | Issue | Test File |
|
||||
|-----------|--------------------------------------------|-----------------------------|
|
||||
| #24 | Position.size as tokens, not USD | edge-cases.test.ts |
|
||||
| #43 | TP1 false detection without price check | price-verification.test.ts |
|
||||
| #45 | Wrong entry price for breakeven SL | breakeven-sl.test.ts |
|
||||
| #52 | ADX-based runner SL positioning | adx-runner-sl.test.ts |
|
||||
| #54 | MAE/MFE as percentages, not dollars | edge-cases.test.ts |
|
||||
| #67 | Duplicate closures (atomic deduplication) | Covered by mock structure |
|
||||
|
||||
## Test Helpers
|
||||
|
||||
### Trade Factory (`tests/helpers/trade-factory.ts`)
|
||||
|
||||
```typescript
|
||||
import { createLongTrade, createShortTrade, createTradeAfterTP1, createTradeAfterTP2 } from '../helpers/trade-factory'
|
||||
|
||||
// Create a basic LONG trade
|
||||
const longTrade = createLongTrade()
|
||||
|
||||
// Create a SHORT trade with custom entry
|
||||
const shortTrade = createShortTrade({ entryPrice: 150 })
|
||||
|
||||
// Create a trade after TP1 hit (40% runner remaining)
|
||||
const runnerTrade = createTradeAfterTP1('long')
|
||||
|
||||
// Create a trade with trailing stop active
|
||||
const trailingTrade = createTradeAfterTP2('short')
|
||||
```
|
||||
|
||||
### Custom Matchers
|
||||
|
||||
```typescript
|
||||
// Check if value is within a range
|
||||
expect(0.86).toBeWithinRange(0.8, 0.9) // passes
|
||||
```
|
||||
|
||||
## Why These Tests Matter
|
||||
|
||||
1. **Financial Protection**: Position Manager handles real money ($540+ capital). Bugs cost real dollars.
|
||||
|
||||
2. **Regression Prevention**: 71+ documented bugs in the codebase. Tests prevent reintroduction.
|
||||
|
||||
3. **Safe Refactoring**: With test coverage, code can be improved without fear of breaking existing functionality.
|
||||
|
||||
4. **Documentation**: Tests serve as executable documentation of expected behavior.
|
||||
|
||||
5. **CI/CD Pipeline**: Automated testing ensures changes don't break critical trading logic.
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Guidelines
|
||||
|
||||
1. **Use Factories**: Always use `createLongTrade()` or `createShortTrade()` instead of manual object creation
|
||||
2. **Test Both Directions**: Every price-based test should cover both LONG and SHORT positions
|
||||
3. **Test Edge Cases**: Include boundary conditions and error scenarios
|
||||
4. **Clear Names**: Test names should describe the exact behavior being tested
|
||||
5. **Reference Pitfalls**: When testing a known bug, reference the pitfall number in comments
|
||||
|
||||
### Example Test
|
||||
|
||||
```typescript
|
||||
import { createLongTrade, createShortTrade, TEST_DEFAULTS } from '../../helpers/trade-factory'
|
||||
|
||||
describe('New Feature', () => {
|
||||
it('should handle LONG position correctly', () => {
|
||||
const trade = createLongTrade()
|
||||
// ... test logic
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
it('should handle SHORT position correctly', () => {
|
||||
const trade = createShortTrade()
|
||||
// ... test logic
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
// Reference known bugs
|
||||
it('should NOT trigger false positive (Pitfall #XX)', () => {
|
||||
// ... regression test
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Mocked Dependencies
|
||||
|
||||
The test setup mocks external dependencies to isolate tests:
|
||||
|
||||
- **Drift Service**: No actual blockchain calls
|
||||
- **Pyth Price Monitor**: No WebSocket connections
|
||||
- **Database Operations**: No actual DB queries
|
||||
- **Telegram Notifications**: No actual messages sent
|
||||
- **Drift Orders**: No actual order placement
|
||||
|
||||
## Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Run a specific test file
|
||||
npm test -- tests/integration/position-manager/tp1-detection.test.ts
|
||||
|
||||
# Run tests matching a pattern
|
||||
npm test -- --testNamePattern="LONG"
|
||||
|
||||
# Run tests in a specific directory
|
||||
npm test -- tests/integration/position-manager/
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
Tests run automatically in CI with:
|
||||
- JUnit XML reports for test results
|
||||
- Coverage reports in HTML and text formats
|
||||
- Failure threshold of 60% coverage
|
||||
|
||||
Configure in `jest.config.js`:
|
||||
|
||||
```javascript
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
functions: 60,
|
||||
lines: 60,
|
||||
statements: 60,
|
||||
},
|
||||
},
|
||||
```
|
||||
247
tests/helpers/trade-factory.ts
Normal file
247
tests/helpers/trade-factory.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Trade Factory - Test Helpers
|
||||
*
|
||||
* Factory functions to create mock trades for Position Manager testing.
|
||||
* Uses realistic values based on actual trading data documented in:
|
||||
* - Problem statement test data
|
||||
* - Common Pitfalls documentation
|
||||
*/
|
||||
|
||||
import { ActiveTrade } from '../../lib/trading/position-manager'
|
||||
|
||||
/**
|
||||
* Default test values based on problem statement:
|
||||
* - LONG: entry $140, TP1 $141.20 (+0.86%), TP2 $142.41 (+1.72%), SL $138.71 (-0.92%)
|
||||
* - SHORT: entry $140, TP1 $138.80 (-0.86%), TP2 $137.59 (-1.72%), SL $141.29 (+0.92%)
|
||||
* - ATR: 0.43, ADX: 26.9, Quality Score: 95, Position Size: $8000
|
||||
*/
|
||||
export const TEST_DEFAULTS = {
|
||||
entry: 140.00,
|
||||
atr: 0.43,
|
||||
adx: 26.9,
|
||||
qualityScore: 95,
|
||||
positionSize: 8000,
|
||||
leverage: 15,
|
||||
|
||||
// Calculated targets for LONG (entry + %)
|
||||
long: {
|
||||
tp1: 141.20, // +0.86%
|
||||
tp2: 142.41, // +1.72%
|
||||
sl: 138.71, // -0.92%
|
||||
emergencySl: 137.20, // -2%
|
||||
},
|
||||
|
||||
// Calculated targets for SHORT (entry - %)
|
||||
short: {
|
||||
tp1: 138.80, // -0.86%
|
||||
tp2: 137.59, // -1.72%
|
||||
sl: 141.29, // +0.92%
|
||||
emergencySl: 142.80, // +2%
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a mock trade
|
||||
*/
|
||||
export interface CreateMockTradeOptions {
|
||||
id?: string
|
||||
positionId?: string
|
||||
symbol?: string
|
||||
direction?: 'long' | 'short'
|
||||
entryPrice?: number
|
||||
positionSize?: number
|
||||
leverage?: number
|
||||
atr?: number
|
||||
adx?: number
|
||||
qualityScore?: number
|
||||
|
||||
// Targets
|
||||
tp1Price?: number
|
||||
tp2Price?: number
|
||||
stopLossPrice?: number
|
||||
emergencyStopPrice?: number
|
||||
|
||||
// State overrides
|
||||
currentSize?: number
|
||||
tp1Hit?: boolean
|
||||
tp2Hit?: boolean
|
||||
slMovedToBreakeven?: boolean
|
||||
slMovedToProfit?: boolean
|
||||
trailingStopActive?: boolean
|
||||
peakPrice?: number
|
||||
|
||||
// P&L tracking
|
||||
realizedPnL?: number
|
||||
unrealizedPnL?: number
|
||||
maxFavorableExcursion?: number
|
||||
maxAdverseExcursion?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique trade ID for testing
|
||||
*/
|
||||
let tradeCounter = 0
|
||||
export function generateTradeId(): string {
|
||||
return `test-trade-${++tradeCounter}-${Date.now()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock ActiveTrade object with sensible defaults
|
||||
*/
|
||||
export function createMockTrade(options: CreateMockTradeOptions = {}): ActiveTrade {
|
||||
const direction = options.direction || 'long'
|
||||
const entryPrice = options.entryPrice || TEST_DEFAULTS.entry
|
||||
const positionSize = options.positionSize || TEST_DEFAULTS.positionSize
|
||||
const targets = direction === 'long' ? TEST_DEFAULTS.long : TEST_DEFAULTS.short
|
||||
|
||||
return {
|
||||
id: options.id || generateTradeId(),
|
||||
positionId: options.positionId || `tx-${Date.now()}`,
|
||||
symbol: options.symbol || 'SOL-PERP',
|
||||
direction,
|
||||
|
||||
// Entry details
|
||||
entryPrice,
|
||||
entryTime: Date.now() - 60000, // Started 1 minute ago
|
||||
positionSize,
|
||||
leverage: options.leverage || TEST_DEFAULTS.leverage,
|
||||
atrAtEntry: options.atr ?? TEST_DEFAULTS.atr,
|
||||
adxAtEntry: options.adx ?? TEST_DEFAULTS.adx,
|
||||
signalQualityScore: options.qualityScore ?? TEST_DEFAULTS.qualityScore,
|
||||
signalSource: 'tradingview',
|
||||
|
||||
// Targets - use provided or calculate from direction
|
||||
stopLossPrice: options.stopLossPrice ?? targets.sl,
|
||||
tp1Price: options.tp1Price ?? targets.tp1,
|
||||
tp2Price: options.tp2Price ?? targets.tp2,
|
||||
emergencyStopPrice: options.emergencyStopPrice ?? targets.emergencySl,
|
||||
|
||||
// State
|
||||
currentSize: options.currentSize ?? positionSize,
|
||||
originalPositionSize: positionSize,
|
||||
takeProfitPrice1: options.tp1Price ?? targets.tp1,
|
||||
takeProfitPrice2: options.tp2Price ?? targets.tp2,
|
||||
tp1Hit: options.tp1Hit ?? false,
|
||||
tp2Hit: options.tp2Hit ?? false,
|
||||
slMovedToBreakeven: options.slMovedToBreakeven ?? false,
|
||||
slMovedToProfit: options.slMovedToProfit ?? false,
|
||||
trailingStopActive: options.trailingStopActive ?? false,
|
||||
|
||||
// P&L tracking
|
||||
realizedPnL: options.realizedPnL ?? 0,
|
||||
unrealizedPnL: options.unrealizedPnL ?? 0,
|
||||
peakPnL: 0,
|
||||
peakPrice: options.peakPrice ?? entryPrice,
|
||||
|
||||
// MAE/MFE tracking (as percentages per Pitfall #54)
|
||||
maxFavorableExcursion: options.maxFavorableExcursion ?? 0,
|
||||
maxAdverseExcursion: options.maxAdverseExcursion ?? 0,
|
||||
maxFavorablePrice: entryPrice,
|
||||
maxAdversePrice: entryPrice,
|
||||
|
||||
// Monitoring
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a LONG position with standard test defaults
|
||||
*/
|
||||
export function createLongTrade(options: Omit<CreateMockTradeOptions, 'direction'> = {}): ActiveTrade {
|
||||
return createMockTrade({
|
||||
...options,
|
||||
direction: 'long',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SHORT position with standard test defaults
|
||||
*/
|
||||
export function createShortTrade(options: Omit<CreateMockTradeOptions, 'direction'> = {}): ActiveTrade {
|
||||
return createMockTrade({
|
||||
...options,
|
||||
direction: 'short',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a trade that has already hit TP1
|
||||
*/
|
||||
export function createTradeAfterTP1(
|
||||
direction: 'long' | 'short',
|
||||
options: Omit<CreateMockTradeOptions, 'direction' | 'tp1Hit'> = {}
|
||||
): ActiveTrade {
|
||||
const positionSize = options.positionSize || TEST_DEFAULTS.positionSize
|
||||
const tp1SizePercent = 60 // Default TP1 closes 60%
|
||||
const remainingSize = positionSize * ((100 - tp1SizePercent) / 100)
|
||||
|
||||
return createMockTrade({
|
||||
...options,
|
||||
direction,
|
||||
tp1Hit: true,
|
||||
slMovedToBreakeven: true,
|
||||
currentSize: remainingSize,
|
||||
// SL moves to entry (breakeven) after TP1 for weak ADX, or adjusted for strong ADX
|
||||
stopLossPrice: options.entryPrice || TEST_DEFAULTS.entry,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a trade that has hit TP2 with trailing stop active
|
||||
*/
|
||||
export function createTradeAfterTP2(
|
||||
direction: 'long' | 'short',
|
||||
options: Omit<CreateMockTradeOptions, 'direction' | 'tp1Hit' | 'tp2Hit' | 'trailingStopActive'> = {}
|
||||
): ActiveTrade {
|
||||
const positionSize = options.positionSize || TEST_DEFAULTS.positionSize
|
||||
const tp1SizePercent = 60
|
||||
const tp2SizePercent = 0 // TP2 as runner - no close at TP2
|
||||
const remainingSize = positionSize * ((100 - tp1SizePercent) / 100)
|
||||
|
||||
const entryPrice = options.entryPrice || TEST_DEFAULTS.entry
|
||||
const targets = direction === 'long' ? TEST_DEFAULTS.long : TEST_DEFAULTS.short
|
||||
|
||||
return createMockTrade({
|
||||
...options,
|
||||
direction,
|
||||
tp1Hit: true,
|
||||
tp2Hit: true,
|
||||
slMovedToBreakeven: true,
|
||||
trailingStopActive: true,
|
||||
currentSize: remainingSize,
|
||||
peakPrice: targets.tp2, // Peak is at TP2 level
|
||||
stopLossPrice: entryPrice, // Breakeven as starting point for trailing
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to calculate expected profit percent
|
||||
*/
|
||||
export function calculateExpectedProfitPercent(
|
||||
entryPrice: number,
|
||||
currentPrice: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return ((currentPrice - entryPrice) / entryPrice) * 100
|
||||
} else {
|
||||
return ((entryPrice - currentPrice) / entryPrice) * 100
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to calculate expected target price
|
||||
*/
|
||||
export function calculateTargetPrice(
|
||||
entryPrice: number,
|
||||
percentChange: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return entryPrice * (1 + percentChange / 100)
|
||||
} else {
|
||||
return entryPrice * (1 - percentChange / 100)
|
||||
}
|
||||
}
|
||||
107
tests/setup.ts
Normal file
107
tests/setup.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Jest Global Test Setup
|
||||
*
|
||||
* Configures the test environment for Position Manager integration tests.
|
||||
* Mocks external dependencies to isolate unit testing.
|
||||
*/
|
||||
|
||||
// Extend Jest matchers if needed
|
||||
expect.extend({
|
||||
toBeWithinRange(received: number, floor: number, ceiling: number) {
|
||||
const pass = received >= floor && received <= ceiling
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
|
||||
pass: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
|
||||
pass: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Declare custom matchers for TypeScript
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toBeWithinRange(floor: number, ceiling: number): R
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock logger to reduce noise during tests
|
||||
jest.mock('../lib/utils/logger', () => ({
|
||||
logger: {
|
||||
log: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock Drift service to avoid network calls
|
||||
jest.mock('../lib/drift/client', () => ({
|
||||
getDriftService: jest.fn(() => ({
|
||||
getPosition: jest.fn(),
|
||||
getOraclePrice: jest.fn(),
|
||||
isInitialized: true,
|
||||
})),
|
||||
initializeDriftService: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock Pyth price monitor
|
||||
jest.mock('../lib/pyth/price-monitor', () => ({
|
||||
getPythPriceMonitor: jest.fn(() => ({
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
getCachedPrice: jest.fn(),
|
||||
getLatestPrice: jest.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock database operations
|
||||
jest.mock('../lib/database/trades', () => ({
|
||||
getOpenTrades: jest.fn(() => Promise.resolve([])),
|
||||
updateTradeExit: jest.fn(() => Promise.resolve()),
|
||||
updateTradeState: jest.fn(() => Promise.resolve()),
|
||||
createTrade: jest.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
// Mock Telegram notifications
|
||||
jest.mock('../lib/notifications/telegram', () => ({
|
||||
sendPositionClosedNotification: jest.fn(() => Promise.resolve()),
|
||||
sendPositionOpenedNotification: jest.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
// Mock Drift orders
|
||||
jest.mock('../lib/drift/orders', () => ({
|
||||
closePosition: jest.fn(() => Promise.resolve({ success: true, realizedPnL: 0 })),
|
||||
cancelAllOrders: jest.fn(() => Promise.resolve({ success: true, cancelledCount: 0 })),
|
||||
placeExitOrders: jest.fn(() => Promise.resolve({ success: true })),
|
||||
}))
|
||||
|
||||
// Mock market data cache
|
||||
jest.mock('../lib/trading/market-data-cache', () => ({
|
||||
getMarketDataCache: jest.fn(() => ({
|
||||
get: jest.fn(() => null),
|
||||
set: jest.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock stop hunt tracker
|
||||
jest.mock('../lib/trading/stop-hunt-tracker', () => ({
|
||||
getStopHuntTracker: jest.fn(() => ({
|
||||
recordStopHunt: jest.fn(() => Promise.resolve()),
|
||||
updateRevengeOutcome: jest.fn(() => Promise.resolve()),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
export {}
|
||||
Reference in New Issue
Block a user