Skip to content

Commit

Permalink
🪢 Chaining matchers (#717)
Browse files Browse the repository at this point in the history
  • Loading branch information
yivlad committed May 4, 2022
1 parent de3905f commit 587ff49
Show file tree
Hide file tree
Showing 16 changed files with 581 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-shrimps-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ethereum-waffle/chai": patch
---

Allow chaining matchers
1 change: 1 addition & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Afterwards update your :code:`package.json` build script:
}
.. group-tab:: Waffle 2.5.0

.. code-block:: json
{
Expand Down
42 changes: 39 additions & 3 deletions docs/source/migration-guides.rst
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,10 @@ In the new Ganache, you should not override the wallet config, otherwise you mig
}
})
Chaining :code:`emit` matchers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Chaining matchers
~~~~~~~~~~~~~~~~~

Now when testing events on a smart contract you can conveniently chain :code:`emit` matchers.
Now when testing events on a smart contract you can conveniently chain matchers. It can be especially useful when testing events.

.. code-block:: ts
Expand All @@ -454,3 +454,39 @@ Now when testing events on a smart contract you can conveniently chain :code:`em
'Two'
)
.to.not.emit(contract, 'Three');
:code:`changeEtherBalance`, :code:`changeEtherBalances`, :code:`changeTokenbalance` and :code:`changeTokenBalances` matchers also support chaining:

.. code-block:: ts
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
.and.to.emit(complex, 'TransferredEther').withArgs(200)
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
Although you may find it more convenient to write multiple expects. The test below is equivalent to the one above:

.. code-block:: ts
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
Note that in both cases you can use :code:`chai` negation :code:`not`. In a case of a single expect everything after :code:`not` is negated.

.. code-block:: ts
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
.and.not
.to.emit(complex, 'UnusedEvent') // This is negated
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
39 changes: 22 additions & 17 deletions waffle-chai/src/matchers/changeBalance.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import {BigNumber, BigNumberish} from 'ethers';
import {Account, getAddressOf} from './misc/account';
import {getBalanceChange} from './changeEtherBalance';
import {transactionPromise} from '../transaction-promise';

export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
Assertion.addMethod('changeBalance', function (
this: any,
account: Account,
balanceChange: BigNumberish
) {
const subject = this._obj;
const derivedPromise = Promise.all([
getBalanceChange(subject, account, {includeFee: true}),
getAddressOf(account)
]).then(
([actualChange, address]) => {
this.assert(
actualChange.eq(BigNumber.from(balanceChange)),
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
`but it has changed by ${actualChange} wei`,
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
balanceChange,
actualChange
);
}
);
transactionPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
return Promise.all([
getBalanceChange(this.txResponse, account, {includeFee: true}),
getAddressOf(account)
]);
}).then(([actualChange, address]: [BigNumber, string]) => {
const isCurrentlyNegated = this.__flags.negate === true;
this.__flags.negate = isNegated;
this.assert(
actualChange.eq(BigNumber.from(balanceChange)),
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
`but it has changed by ${actualChange} wei`,
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
balanceChange,
actualChange
);
this.__flags.negate = isCurrentlyNegated;
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
this.txPromise = derivedPromise;
return this;
});
}
44 changes: 24 additions & 20 deletions waffle-chai/src/matchers/changeBalances.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {BigNumber, BigNumberish} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {getBalanceChanges} from './changeEtherBalances';
import {Account} from './misc/account';
import {getAddresses} from './misc/balance';
Expand All @@ -9,28 +10,31 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
accounts: Account[],
balanceChanges: BigNumberish[]
) {
const subject = this._obj;

const derivedPromise = Promise.all([
getBalanceChanges(subject, accounts, {includeFee: true}),
getAddresses(accounts)
]).then(
([actualChanges, accountAddresses]) => {
this.assert(
actualChanges.every((change, ind) =>
change.eq(BigNumber.from(balanceChanges[ind]))
),
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
`but it has changed by ${actualChanges} wei`,
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
balanceChanges.map((balanceChange) => balanceChange.toString()),
actualChanges.map((actualChange) => actualChange.toString())
);
}
);
transactionPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
return Promise.all([
getBalanceChanges(this.txResponse, accounts, {includeFee: true}),
getAddresses(accounts)
]);
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
const isCurrentlyNegated = this.__flags.negate === true;
this.__flags.negate = isNegated;
this.assert(
actualChanges.every((change, ind) =>
change.eq(BigNumber.from(balanceChanges[ind]))
),
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
`but it has changed by ${actualChanges} wei`,
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
balanceChanges.map((balanceChange) => balanceChange.toString()),
actualChanges.map((actualChange) => actualChange.toString())
);
this.__flags.negate = isCurrentlyNegated;
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
this.txPromise = derivedPromise;
return this;
});
}
56 changes: 26 additions & 30 deletions waffle-chai/src/matchers/changeEtherBalance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {BigNumber, BigNumberish, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {ensure} from './calledOnContract/utils';
import {Account, getAddressOf} from './misc/account';
import {BalanceChangeOptions} from './misc/balance';
Expand All @@ -10,53 +11,48 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
balanceChange: BigNumberish,
options: BalanceChangeOptions
) {
const subject = this._obj;
const derivedPromise = Promise.all([
getBalanceChange(subject, account, options),
getAddressOf(account)
]).then(
([actualChange, address]) => {
this.assert(
actualChange.eq(BigNumber.from(balanceChange)),
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
transactionPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
return Promise.all([
getBalanceChange(this.txResponse, account, options),
getAddressOf(account)
]);
}).then(([actualChange, address]: [BigNumber, string]) => {
const isCurrentlyNegated = this.__flags.negate === true;
this.__flags.negate = isNegated;
this.assert(
actualChange.eq(BigNumber.from(balanceChange)),
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
`but it has changed by ${actualChange} wei`,
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
balanceChange,
actualChange
);
}
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
balanceChange,
actualChange
);
this.__flags.negate = isCurrentlyNegated;
}
);
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
this.txPromise = derivedPromise;
return this;
});
}

export async function getBalanceChange(
transaction:
| providers.TransactionResponse
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
txResponse: providers.TransactionResponse,
account: Account,
options?: BalanceChangeOptions
) {
ensure(account.provider !== undefined, TypeError, 'Provider not found');

let txResponse: providers.TransactionResponse;

if (typeof transaction === 'function') {
txResponse = await transaction();
} else {
txResponse = transaction;
}

const txReceipt = await txResponse.wait();
const txBlockNumber = txReceipt.blockNumber;
const address = await getAddressOf(account);

const balanceAfter = await account.provider.getBalance(getAddressOf(account), txBlockNumber);
const balanceBefore = await account.provider.getBalance(getAddressOf(account), txBlockNumber - 1);
const balanceAfter = await account.provider.getBalance(address, txBlockNumber);
const balanceBefore = await account.provider.getBalance(address, txBlockNumber - 1);

if (options?.includeFee !== true && await getAddressOf(account) === txResponse.from) {
if (options?.includeFee !== true && address === txReceipt.from) {
const gasPrice = txResponse.gasPrice ?? txReceipt.effectiveGasPrice;
const gasUsed = txReceipt.gasUsed;
const txFee = gasPrice.mul(gasUsed);
Expand Down
56 changes: 25 additions & 31 deletions waffle-chai/src/matchers/changeEtherBalances.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {BigNumber, BigNumberish, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {getAddressOf, Account} from './misc/account';
import {BalanceChangeOptions, getAddresses, getBalances} from './misc/balance';

Expand All @@ -9,47 +10,40 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
balanceChanges: BigNumberish[],
options: BalanceChangeOptions
) {
const subject = this._obj;

const derivedPromise = Promise.all([
getBalanceChanges(subject, accounts, options),
getAddresses(accounts)
]).then(
([actualChanges, accountAddresses]) => {
this.assert(
actualChanges.every((change, ind) =>
change.eq(BigNumber.from(balanceChanges[ind]))
),
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
`but it has changed by ${actualChanges} wei`,
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
balanceChanges.map((balanceChange) => balanceChange.toString()),
actualChanges.map((actualChange) => actualChange.toString())
);
}
);
transactionPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
return Promise.all([
getBalanceChanges(this.txResponse, accounts, options),
getAddresses(accounts)
]);
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
const isCurrentlyNegated = this.__flags.negate === true;
this.__flags.negate = isNegated;
this.assert(
actualChanges.every((change, ind) =>
change.eq(BigNumber.from(balanceChanges[ind]))
),
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
`but it has changed by ${actualChanges} wei`,
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
balanceChanges.map((balanceChange) => balanceChange.toString()),
actualChanges.map((actualChange) => actualChange.toString())
);
this.__flags.negate = isCurrentlyNegated;
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
this.txPromise = derivedPromise;
return this;
});
}

export async function getBalanceChanges(
transaction:
| providers.TransactionResponse
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
txResponse: providers.TransactionResponse,
accounts: Account[],
options: BalanceChangeOptions
) {
let txResponse: providers.TransactionResponse;

if (typeof transaction === 'function') {
txResponse = await transaction();
} else {
txResponse = transaction;
}

const txReceipt = await txResponse.wait();
const txBlockNumber = txReceipt.blockNumber;

Expand Down

0 comments on commit 587ff49

Please sign in to comment.