@tevm/test-matchers
Version:
Vite test matchers for Tevm or EVM-related testing in TypeScript.
213 lines (200 loc) • 7.65 kB
text/typescript
import type { AbiParametersToPrimitiveTypes, ExtractAbiError } from 'abitype'
import type { Abi, AbiParameter, Client, ContractErrorName, Hex } from 'viem'
import { createChainableFromVitest } from '../../chainable/chainable.js'
import type { ChainableAssertion } from '../../chainable/types.js'
import type { AbiInputsToNamedArgs, ContainsContractAbi } from '../../common/types.js'
import { toBeReverted } from './toBeReverted.js'
import { toBeRevertedWithError } from './toBeRevertedWithError.js'
import { toBeRevertedWithString } from './toBeRevertedWithString.js'
import { withErrorArgs } from './withErrorArgs.js'
import { withErrorNamedArgs } from './withErrorNamedArgs.js'
// Create chainable matchers
export const toBeRevertedWithErrorChainable = createChainableFromVitest({
name: 'toBeRevertedWithError' as const,
vitestMatcher: toBeRevertedWithError,
})
export const withErrorArgsChainable = createChainableFromVitest({
name: 'withErrorArgs' as const,
vitestMatcher: withErrorArgs,
})
export const withErrorNamedArgsChainable = createChainableFromVitest({
name: 'withErrorNamedArgs' as const,
vitestMatcher: withErrorNamedArgs,
})
// Register the chainable matchers
export const chainableErrorMatchers = {
toBeRevertedWithError: toBeRevertedWithErrorChainable,
withErrorArgs: withErrorArgsChainable,
withErrorNamedArgs: withErrorNamedArgsChainable,
}
export { toBeReverted, toBeRevertedWithString, toBeRevertedWithError, withErrorArgs, withErrorNamedArgs }
// TypeScript declaration for vitest
export interface ErrorMatchers {
/**
* Asserts that a transaction reverted for any reason.
* This includes string reverts, custom errors, and panics.
*
* @param client - Optional client for transaction execution
*
* @example
* ```typescript
* // Test any revert
* await expect(writeContract(client, contract.write.failingFunction()))
* .toBeReverted(client)
*
* // Works with .not for successful transactions
* await expect(writeContract(client, contract.write.successfulFunction()))
* .not.toBeReverted(client)
* ```
*
* @see {@link toBeRevertedWithString} for specific revert messages
* @see {@link toBeRevertedWithError} for custom errors
*/
toBeReverted(client?: Client): Promise<void>
/**
* Asserts that a transaction reverted with a specific revert string.
* Use this for `revert("message")` style reverts.
*
* @param client - Client for transaction execution
* @param revertString - Expected exact revert message
*
* @example
* ```typescript
* // Contract: require(amount > 0, "Amount must be positive")
* await expect(writeContract(client, contract.write.transfer(to, 0n)))
* .toBeRevertedWithString(client, 'Amount must be positive')
*
* // Note: Message must match exactly
* await expect(transaction)
* .not.toBeRevertedWithString(client, 'Different message')
* ```
*
* @see {@link toBeRevertedWithError} for custom errors
*/
toBeRevertedWithString(client: Client, revertString: string): Promise<void>
/**
* Asserts that a transaction reverted with a specific custom error.
* Use this for custom error types, not `revert()` strings.
*
* @param client - Client for transaction execution
* @param contract - Contract object with ABI containing the error
* @param errorName - Name of the custom error in the ABI
*
* @example
* ```typescript
* // error InsufficientBalance(uint256 available, uint256 required);
* await expect(writeContract(client, contract.write.transfer(to, 1000n)))
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
*
* // Chain with argument assertions
* await expect(writeContract(client, contract.write.transfer(to, 1000n)))
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
* .withErrorArgs(50n, 1000n)
*
* // Chain with argument assertions by name (partial matching)
* await expect(writeContract(client, contract.write.transfer(to, 1000n)))
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
* .withErrorNamedArgs({ required: 1000n })
* ```
*
* @see {@link withErrorArgs} to test error arguments
* @see {@link withErrorNamedArgs} to test error arguments by name
* @see {@link toBeRevertedWithString} for revert strings
*/
toBeRevertedWithError<TAbi extends Abi, TErrorName extends ContractErrorName<TAbi>>(
client: Client,
contract: ContainsContractAbi<TAbi>,
errorName: TErrorName,
): Promise<ErrorAssertionWithContract<TAbi, TErrorName>> & ErrorAssertionWithContract<TAbi, TErrorName>
/**
* Asserts that a transaction reverted with an error matching the signature.
*
* @param client - Client for transaction execution
* @param errorName - Error signature string (e.g., "InsufficientBalance(uint256,uint256)")
*
* @example
* ```typescript
* await expect(transaction)
* .toBeRevertedWithError(client, 'InsufficientBalance(uint256,uint256)')
* .withErrorArgs(50n, 1000n)
* ```
*/
toBeRevertedWithError(client: Client, errorName: string): ChainableAssertion
/**
* Asserts that a transaction reverted with an error matching the selector.
*
* @param client - Client for transaction execution
* @param errorSelector - Error selector (4-byte hex)
*
* @example
* ```typescript
* await expect(transaction)
* .toBeRevertedWithError(client, '0x356680b7') // InsufficientBalance selector
* ```
*/
toBeRevertedWithError(client: Client, errorSelector: Hex): ChainableAssertion
}
// Only-after toBeRevertedWithError assertion type
interface ErrorAssertionWithContract<TAbi extends Abi, TErrorName extends ContractErrorName<TAbi>> {
/**
* Chains with toBeRevertedWithError to assert error arguments in positional order.
* Arguments must match exactly in the order they appear in the error.
*
* **Limitation**: Cannot use .not before this method.
*
* @param expectedArgs - Expected arguments in order
*
* @example
* ```typescript
* // error InsufficientBalance(uint256 available, uint256 required);
* await expect(transaction)
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
* .withErrorArgs(
* 50n, // available
* 1000n // required
* )
* ```
*
* @see {@link withErrorNamedArgs} for partial matching by name
*/
withErrorArgs<
TInputs extends readonly AbiParameter[] = ExtractAbiError<TAbi, TErrorName> extends {
inputs: infer U extends readonly AbiParameter[]
}
? U
: readonly AbiParameter[],
>(...expectedArgs: AbiParametersToPrimitiveTypes<TInputs>): ChainableAssertion
/**
* Chains with toBeRevertedWithError to assert error arguments by name.
* Supports partial matching - only specified arguments are checked.
*
* **Limitation**: Cannot use .not before this method.
*
* @param expectedArgs - Object with expected named arguments (partial)
*
* @example
* ```typescript
* // Check only specific arguments
* await expect(transaction)
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
* .withErrorNamedArgs({ required: 1000n })
*
* // Check all arguments
* await expect(transaction)
* .toBeRevertedWithError(client, contract, 'InsufficientBalance')
* .withErrorNamedArgs({
* available: 50n,
* required: 1000n
* })
* ```
*
* @see {@link withErrorArgs} for positional argument matching
*/
withErrorNamedArgs<
TInputs extends readonly AbiParameter[] = ExtractAbiError<TAbi, TErrorName> extends {
inputs: infer U extends readonly AbiParameter[]
}
? U
: readonly AbiParameter[],
>(expectedArgs: Partial<AbiInputsToNamedArgs<TInputs>>): ChainableAssertion
}