Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions packages/perps-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
branches: 70,
functions: 78,
lines: 80,
statements: 80,
},
},
}),

// Coverage is scoped to the placeholder test file only (not synced source).
// The real source files are synced from Mobile and tested there.
// When tests are migrated from Mobile to Core, restore this to
// the default ('./src/**/*.ts') and raise thresholds accordingly.
// Coverage is collected from real source files. Barrel files are excluded
// because they only re-export the tested modules.
// Applied after merge to fully replace (not concat) the base array.
collectCoverageFrom: ['./tests/placeholder.test.ts'],
collectCoverageFrom: ['./src/**/*.ts', '!./src/**/index.ts'],
};
106 changes: 106 additions & 0 deletions packages/perps-controller/tests/helpers/providerMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* eslint-disable */
Comment thread
abretonc7s marked this conversation as resolved.
/**
* Shared provider mocks for Perps tests
* Provides reusable mock implementations for HyperLiquidProvider and related interfaces
*/
import { type HyperLiquidProvider } from '@metamask/perps-controller';

export const createMockHyperLiquidProvider =
(): jest.Mocked<HyperLiquidProvider> =>
({
protocolId: 'hyperliquid',
initialize: jest.fn(),
isReadyToTrade: jest.fn(),
toggleTestnet: jest.fn(),
getPositions: jest.fn(),
getAccountState: jest.fn(),
getHistoricalPortfolio: jest.fn().mockResolvedValue({
totalBalance24hAgo: '10000',
totalBalance7dAgo: '9500',
totalBalance30dAgo: '9000',
}),
getMarkets: jest.fn(),
placeOrder: jest.fn(),
editOrder: jest.fn(),
cancelOrder: jest.fn(),
cancelOrders: jest.fn(),
closePosition: jest.fn(),
closePositions: jest.fn(),
withdraw: jest.fn(),
getDepositRoutes: jest.fn(),
getWithdrawalRoutes: jest.fn(),
validateDeposit: jest.fn().mockResolvedValue({ isValid: true }),
validateOrder: jest.fn().mockResolvedValue({ isValid: true }),
validateClosePosition: jest.fn().mockResolvedValue({ isValid: true }),
validateWithdrawal: jest.fn().mockResolvedValue({ isValid: true }),
subscribeToPrices: jest.fn(),
subscribeToPositions: jest.fn(),
subscribeToOrderFills: jest.fn(),
setLiveDataConfig: jest.fn(),
disconnect: jest.fn(),
updatePositionTPSL: jest.fn(),
calculateLiquidationPrice: jest.fn(),
calculateMaintenanceMargin: jest.fn(),
getMaxLeverage: jest.fn(),
calculateFees: jest.fn(),
getMarketDataWithPrices: jest.fn(),
getBlockExplorerUrl: jest.fn(),
getOrderFills: jest.fn(),
getOrders: jest.fn(),
getFunding: jest.fn(),
getCurrentAccountId: jest
.fn()
.mockResolvedValue(
'eip155:1:0x0000000000000000000000000000000000000001',
),
getIsFirstTimeUser: jest.fn(),
getOpenOrders: jest.fn(),
subscribeToOrders: jest.fn(),
subscribeToAccount: jest.fn(),
setUserFeeDiscount: jest.fn(),
// WebSocket connection state methods
getWebSocketConnectionState: jest.fn(),
subscribeToConnectionState: jest.fn().mockReturnValue(() => undefined),
reconnect: jest.fn().mockResolvedValue(undefined),
}) as unknown as jest.Mocked<HyperLiquidProvider>;

export const createMockOrder = (overrides = {}) => ({
orderId: 'order-1',
symbol: 'BTC',
side: 'buy' as const,
orderType: 'limit' as const,
size: '0.1',
originalSize: '0.1',
price: '50000',
filledSize: '0',
remainingSize: '0.1',
status: 'open' as const,
timestamp: Date.now(),
...overrides,
});
Comment thread
cursor[bot] marked this conversation as resolved.

export const createMockPosition = (overrides = {}) => ({
symbol: 'BTC',
size: '0.5',
entryPrice: '50000',
positionValue: '25000',
unrealizedPnl: '100',
marginUsed: '1000',
leverage: { type: 'cross' as const, value: 25 },
liquidationPrice: '48000',
maxLeverage: 50,
returnOnEquity: '10',
cumulativeFunding: {
allTime: '0',
sinceOpen: '0',
sinceChange: '0',
},
roi: '10',
takeProfitPrice: undefined,
stopLossPrice: undefined,
takeProfitCount: 0,
stopLossCount: 0,
marketPrice: '50200',
timestamp: Date.now(),
...overrides,
});
246 changes: 246 additions & 0 deletions packages/perps-controller/tests/helpers/serviceMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/* eslint-disable */
/**
* Shared service mocks for Perps service tests
* Provides reusable mock implementations for ServiceContext and related types
*/

import {
type ServiceContext,
type PerpsControllerState,
type InitializationState,
type PerpsControllerMessenger,
type PerpsPlatformDependencies,
} from '@metamask/perps-controller';

/**
* Create a mock EVM account (KeyringAccount)
*/
export const createMockEvmAccount = () => ({
id: '00000000-0000-0000-0000-000000000000',
address: '0x1234567890abcdef1234567890abcdef12345678' as `0x${string}`,
type: 'eip155:eoa' as const,
options: {},
scopes: ['eip155:1'],
methods: ['eth_signTransaction', 'eth_sign'],
metadata: {
name: 'Test Account',
importTime: Date.now(),
keyring: { type: 'HD Key Tree' },
},
});

/**
* Create a mock PerpsPlatformDependencies instance.
* Returns a type-safe mock with jest.Mock functions for all methods.
* Uses `as unknown as jest.Mocked<PerpsPlatformDependencies>` pattern
* to ensure compatibility with both the interface contract and Jest mock APIs.
*
* Architecture:
* - Observability: logger, debugLogger, metrics, performance, tracer (stateless utilities)
* - Platform: streamManager (mobile/extension specific capabilities)
* - Controllers: consolidated access to all external controllers
*/
export const createMockInfrastructure =
(): jest.Mocked<PerpsPlatformDependencies> =>
({
// === Observability (stateless utilities) ===
logger: {
error: jest.fn(),
},
debugLogger: {
log: jest.fn(),
},
metrics: {
trackEvent: jest.fn(),
isEnabled: jest.fn(() => true),
trackPerpsEvent: jest.fn(),
},
performance: {
now: jest.fn(() => Date.now()),
},
tracer: {
trace: jest.fn(() => undefined),
endTrace: jest.fn(),
setMeasurement: jest.fn(),
addBreadcrumb: jest.fn(),
},

// === Platform Services ===
streamManager: {
pauseChannel: jest.fn(),
resumeChannel: jest.fn(),
clearAllChannels: jest.fn(),
},

// === Feature Flags (platform-specific version gating) ===
featureFlags: {
validateVersionGated: jest.fn().mockReturnValue(undefined),
},

// === Market Data Formatting ===
marketDataFormatters: {
formatVolume: jest.fn((v: number) => `$${v.toFixed(0)}`),
formatPerpsFiat: jest.fn((v: number) => `$${v.toFixed(2)}`),
formatPercentage: jest.fn((p: number) => `${p.toFixed(2)}%`),
priceRangesUniversal: [],
},

// === Cache Invalidation ===
cacheInvalidator: {
invalidate: jest.fn(),
invalidateAll: jest.fn(),
},

// === Rewards (DI — no RewardsController in Core yet) ===
rewards: {
getPerpsDiscountForAccount: jest.fn().mockResolvedValue(0),
},

// === Disk Cache (cold-start persistence) ===
diskCache: {
getItem: jest.fn().mockResolvedValue(null),
getItemSync: jest.fn().mockReturnValue(null),
setItem: jest.fn().mockResolvedValue(undefined),
removeItem: jest.fn().mockResolvedValue(undefined),
},
}) as unknown as jest.Mocked<PerpsPlatformDependencies>;

/**
* Create a mock PerpsControllerState
*/
export const createMockPerpsControllerState = (
overrides: Partial<PerpsControllerState> = {},
): PerpsControllerState => ({
activeProvider: 'hyperliquid',
isTestnet: false,
initializationState: 'initialized' as InitializationState,
initializationError: null,
initializationAttempts: 0,
accountState: null,
perpsBalances: {},
depositInProgress: false,
lastDepositTransactionId: null,
lastDepositResult: null,
withdrawInProgress: false,
lastWithdrawResult: null,
lastCompletedWithdrawalTimestamp: null,
lastCompletedWithdrawalTxHashes: [],
withdrawalRequests: [],
withdrawalProgress: {
progress: 0,
lastUpdated: 0,
activeWithdrawalId: null,
},
depositRequests: [],
isEligible: true,
isFirstTimeUser: {
testnet: true,
mainnet: true,
},
hasPlacedFirstOrder: {
testnet: false,
mainnet: false,
},
watchlistMarkets: {
testnet: [],
mainnet: [],
},
tradeConfigurations: {
testnet: {},
mainnet: {},
},
marketFilterPreferences: {
optionId: 'volume',
direction: 'desc',
},
lastError: null,
lastUpdateTimestamp: Date.now(),
hip3ConfigVersion: 0,
selectedPaymentToken: null,
cachedMarketDataByProvider: {},
cachedUserDataByProvider: {},
...overrides,
});

/**
* Create a mock ServiceContext with optional overrides
* Note: infrastructure is no longer part of ServiceContext - it's now injected
* into service instances via constructor.
*/
export const createMockServiceContext = (
overrides: Partial<ServiceContext> = {},
): ServiceContext => ({
tracingContext: {
provider: 'hyperliquid',
isTestnet: false,
},
errorContext: {
controller: 'TestService',
method: 'testMethod',
},
stateManager: {
update: jest.fn(),
getState: jest.fn(() => createMockPerpsControllerState()),
},
...overrides,
});

/**
* Create a mock PerpsControllerMessenger for testing inter-controller communication.
* The messenger.call() method should be configured in each test to return appropriate values.
*
* Common messenger actions used:
* - 'AccountTreeController:getAccountsFromSelectedAccountGroup' - returns array of accounts
* - 'KeyringController:signTypedMessage' - returns signature string
* - 'NetworkController:getState' - returns { selectedNetworkClientId: string }
* - 'NetworkController:getNetworkClientById' - returns { configuration: { chainId: string } }
* - 'AuthenticationController:getBearerToken' - returns bearer token string
*
* @param overrides - Optional partial messenger to override default behavior
*/
export const createMockMessenger = (
overrides?: Partial<PerpsControllerMessenger>,
): jest.Mocked<PerpsControllerMessenger> => {
const mockEvmAccount = createMockEvmAccount();
const base = {
call: jest.fn().mockImplementation((action: string) => {
// Default implementations for common actions
if (
action === 'AccountTreeController:getAccountsFromSelectedAccountGroup'
) {
return [mockEvmAccount];
}
if (action === 'KeyringController:getState') {
return { isUnlocked: true };
}
if (action === 'KeyringController:signTypedMessage') {
return Promise.resolve('0xSignatureResult');
}
if (action === 'NetworkController:getState') {
return { selectedNetworkClientId: 'mainnet' };
}
if (action === 'NetworkController:getNetworkClientById') {
return { configuration: { chainId: '0x1' } };
}
if (action === 'AuthenticationController:getBearerToken') {
return Promise.resolve('mock-bearer-token');
}
return undefined;
}),
publish: jest.fn(),
subscribe: jest.fn(),
unsubscribe: jest.fn(),
registerActionHandler: jest.fn(),
registerMethodActionHandlers: jest.fn(),
unregisterActionHandler: jest.fn(),
// Additional methods used by PerpsController
registerEventHandler: jest.fn(),
registerInitialEventPayload: jest.fn(),
unregisterEventHandler: jest.fn(),
clearEventSubscriptions: jest.fn(),
};
return {
...base,
...overrides,
} as unknown as jest.Mocked<PerpsControllerMessenger>;
};
Loading
Loading