Last active
March 18, 2024 11:06
-
-
Save mesquka/529627ab5482580df8fb1e7f559b9322 to your computer and use it in GitHub Desktop.
Chai ethersjs v5 struct/return value matcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Result } from '@ethersproject/abi'; | |
import chaiLocal from 'chai'; | |
import { ethers } from 'ethers'; | |
declare global { | |
// eslint-disable-next-line @typescript-eslint/no-namespace | |
export namespace Chai { | |
interface Assertion { | |
matchStruct(expected: unknown): void; | |
} | |
} | |
} | |
/** | |
* Recursively check ethers return object to ensure values match | |
* @param chai - chai object | |
* @param ethersValue - ethers value | |
* @param expectedValue - expected value | |
* @returns matches | |
*/ | |
function matchStruct(chai: Chai.ChaiStatic, ethersValue: unknown, expectedValue: unknown) { | |
const { expect } = chai; | |
// 'Magic' match any set value | |
if (expectedValue === 'any' && ethersValue) { | |
return true; | |
} | |
// TODO: ethers v6 update, change to using Result.toObject() | |
// This works fine for the current contracts as no keys are numbers in any structs | |
if (Array.isArray(ethersValue) && ethersValue.length !== Object.keys(ethersValue).length) { | |
// If array, and array length is not equal to keys length, this is an object | |
// Assert expected value is also an object | |
expect(expectedValue).to.be.an('object'); | |
// Cast expected value to object | |
const expectedValueAsObject = expectedValue as Record<string, unknown>; | |
// Cast ethersValue to object | |
const ethersValueAsObject = ethersValue as unknown as Record<string, unknown>; | |
// Test all expected value keys exist on ethers value | |
Object.keys(expectedValueAsObject).forEach((key) => { | |
expect(ethersValueAsObject).to.have.property(key); | |
}); | |
// Filter out numerical values (array position keys), recursively test remaining | |
Object.keys(ethersValueAsObject) | |
.filter((key) => !/^\d+$/.test(key)) | |
.forEach((key) => { | |
// Assert expected value also has key | |
expect(expectedValue).to.have.property(key); | |
// Recursively test sub-objects | |
matchStruct(chai, ethersValueAsObject[key] as Result, expectedValueAsObject[key]); | |
}); | |
return true; | |
} | |
// Is regular array | |
if (Array.isArray(ethersValue)) { | |
// Assert expected value is also an array | |
expect(expectedValue).to.be.an('array'); | |
// Cast expected value to array | |
const expectedValueAsArray = expectedValue as unknown[]; | |
// Assert array lengths are the same | |
expect(ethersValue.length).to.equal(expectedValueAsArray.length); | |
// Check each array value recursively | |
ethersValue.forEach((value, i) => matchStruct(chai, value, expectedValueAsArray[i])); | |
return true; | |
} | |
// Check if string | |
if (typeof ethersValue === 'string') { | |
// If expected value is strictly equal (is string and same value), no further checks needed | |
if (ethersValue === expectedValue) return true; | |
// If string test doesn't match, check if ethers value is bytes (address is treated as bytes) | |
// eslint-disable-next-line @typescript-eslint/no-unused-expressions | |
expect(ethersValue.startsWith('0x'), "String doesn't match, and not bytes").to.be.true; | |
// Check bytes string matches | |
if (typeof expectedValue === 'string') { | |
// Ensure 0x prefixed | |
const expectedValuePrefixed = expectedValue.startsWith('0x') | |
? expectedValue | |
: `0x${expectedValue}`; | |
// Check value matches | |
expect(ethersValue.toLowerCase()).to.equal(expectedValuePrefixed.toLowerCase()); | |
return true; | |
} | |
// Check bytes array matches | |
if (expectedValue instanceof Uint8Array) { | |
// Convert to 0x prefixed hex string | |
const expectedValuePrefixed = ethers.hexlify(expectedValue); | |
// Check value matches | |
expect(ethersValue.toLowerCase()).to.equal(expectedValuePrefixed.toLowerCase()); | |
return true; | |
} | |
expect.fail( | |
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
`${ethersValue} (${typeof ethersValue}) isn't bytes, but ${expectedValue} (${typeof expectedValue}) is not`, | |
); | |
} | |
// Numerical value | |
if (typeof ethersValue === 'number' || typeof ethersValue === 'bigint') { | |
// Check numerical values match | |
expect(ethersValue).to.equal(expectedValue); | |
return true; | |
} | |
// Boolean value | |
if (typeof ethersValue === 'boolean') { | |
// Check numerical values match | |
expect(ethersValue).to.equal(expectedValue); | |
return true; | |
} | |
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
expect.fail(`${ethersValue} (${typeof ethersValue}) isn't a known type`); | |
return false; | |
} | |
/** | |
* Generates withArgs matcher | |
* @param expectedValue - expected value | |
* @returns withArgs matcher | |
*/ | |
function argStruct(expectedValue: unknown) { | |
return function (ethersValue: unknown) { | |
return matchStruct(chaiLocal, ethersValue, expectedValue); | |
}; | |
} | |
chaiLocal.use((chai) => { | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
chai.Assertion.addMethod('matchStruct', function (expectedValue: Result) { | |
matchStruct(chai, this._obj, expectedValue); | |
}); | |
}); | |
export { argStruct }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment