Skip to content

Commit 587ff49

Browse files
authored
🪢 Chaining matchers (#717)
1 parent de3905f commit 587ff49

16 files changed

Lines changed: 581 additions & 164 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ethereum-waffle/chai": patch
3+
---
4+
5+
Allow chaining matchers

‎docs/source/configuration.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Afterwards update your :code:`package.json` build script:
4949
}
5050
5151
.. group-tab:: Waffle 2.5.0
52+
5253
.. code-block:: json
5354
5455
{

‎docs/source/migration-guides.rst‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,10 @@ In the new Ganache, you should not override the wallet config, otherwise you mig
436436
}
437437
})
438438
439-
Chaining :code:`emit` matchers
440-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
439+
Chaining matchers
440+
~~~~~~~~~~~~~~~~~
441441

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

444444
.. code-block:: ts
445445
@@ -454,3 +454,39 @@ Now when testing events on a smart contract you can conveniently chain :code:`em
454454
'Two'
455455
)
456456
.to.not.emit(contract, 'Three');
457+
458+
:code:`changeEtherBalance`, :code:`changeEtherBalances`, :code:`changeTokenbalance` and :code:`changeTokenBalances` matchers also support chaining:
459+
460+
.. code-block:: ts
461+
462+
await token.approve(complex.address, 100);
463+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
464+
await expect(tx)
465+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
466+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
467+
.and.to.emit(complex, 'TransferredEther').withArgs(200)
468+
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
469+
470+
Although you may find it more convenient to write multiple expects. The test below is equivalent to the one above:
471+
472+
.. code-block:: ts
473+
474+
await token.approve(complex.address, 100);
475+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
476+
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
477+
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
478+
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
479+
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
480+
481+
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.
482+
483+
.. code-block:: ts
484+
485+
await token.approve(complex.address, 100);
486+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
487+
await expect(expect(tx)
488+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
489+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
490+
.and.not
491+
.to.emit(complex, 'UnusedEvent') // This is negated
492+
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
import {BigNumber, BigNumberish} from 'ethers';
22
import {Account, getAddressOf} from './misc/account';
33
import {getBalanceChange} from './changeEtherBalance';
4+
import {transactionPromise} from '../transaction-promise';
45

56
export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
67
Assertion.addMethod('changeBalance', function (
78
this: any,
89
account: Account,
910
balanceChange: BigNumberish
1011
) {
11-
const subject = this._obj;
12-
const derivedPromise = Promise.all([
13-
getBalanceChange(subject, account, {includeFee: true}),
14-
getAddressOf(account)
15-
]).then(
16-
([actualChange, address]) => {
17-
this.assert(
18-
actualChange.eq(BigNumber.from(balanceChange)),
19-
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
20-
`but it has changed by ${actualChange} wei`,
21-
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
22-
balanceChange,
23-
actualChange
24-
);
25-
}
26-
);
12+
transactionPromise(this);
13+
const isNegated = this.__flags.negate === true;
14+
const derivedPromise = this.txPromise.then(() => {
15+
return Promise.all([
16+
getBalanceChange(this.txResponse, account, {includeFee: true}),
17+
getAddressOf(account)
18+
]);
19+
}).then(([actualChange, address]: [BigNumber, string]) => {
20+
const isCurrentlyNegated = this.__flags.negate === true;
21+
this.__flags.negate = isNegated;
22+
this.assert(
23+
actualChange.eq(BigNumber.from(balanceChange)),
24+
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
25+
`but it has changed by ${actualChange} wei`,
26+
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
27+
balanceChange,
28+
actualChange
29+
);
30+
this.__flags.negate = isCurrentlyNegated;
31+
});
2732
this.then = derivedPromise.then.bind(derivedPromise);
2833
this.catch = derivedPromise.catch.bind(derivedPromise);
29-
this.promise = derivedPromise;
34+
this.txPromise = derivedPromise;
3035
return this;
3136
});
3237
}
Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {getBalanceChanges} from './changeEtherBalances';
34
import {Account} from './misc/account';
45
import {getAddresses} from './misc/balance';
@@ -9,28 +10,31 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
910
accounts: Account[],
1011
balanceChanges: BigNumberish[]
1112
) {
12-
const subject = this._obj;
13-
14-
const derivedPromise = Promise.all([
15-
getBalanceChanges(subject, accounts, {includeFee: true}),
16-
getAddresses(accounts)
17-
]).then(
18-
([actualChanges, accountAddresses]) => {
19-
this.assert(
20-
actualChanges.every((change, ind) =>
21-
change.eq(BigNumber.from(balanceChanges[ind]))
22-
),
23-
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
24-
`but it has changed by ${actualChanges} wei`,
25-
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
26-
balanceChanges.map((balanceChange) => balanceChange.toString()),
27-
actualChanges.map((actualChange) => actualChange.toString())
28-
);
29-
}
30-
);
13+
transactionPromise(this);
14+
const isNegated = this.__flags.negate === true;
15+
const derivedPromise = this.txPromise.then(() => {
16+
return Promise.all([
17+
getBalanceChanges(this.txResponse, accounts, {includeFee: true}),
18+
getAddresses(accounts)
19+
]);
20+
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
21+
const isCurrentlyNegated = this.__flags.negate === true;
22+
this.__flags.negate = isNegated;
23+
this.assert(
24+
actualChanges.every((change, ind) =>
25+
change.eq(BigNumber.from(balanceChanges[ind]))
26+
),
27+
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
28+
`but it has changed by ${actualChanges} wei`,
29+
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
30+
balanceChanges.map((balanceChange) => balanceChange.toString()),
31+
actualChanges.map((actualChange) => actualChange.toString())
32+
);
33+
this.__flags.negate = isCurrentlyNegated;
34+
});
3135
this.then = derivedPromise.then.bind(derivedPromise);
3236
this.catch = derivedPromise.catch.bind(derivedPromise);
33-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3438
return this;
3539
});
3640
}

‎waffle-chai/src/matchers/changeEtherBalance.ts‎

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {ensure} from './calledOnContract/utils';
34
import {Account, getAddressOf} from './misc/account';
45
import {BalanceChangeOptions} from './misc/balance';
@@ -10,53 +11,48 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
1011
balanceChange: BigNumberish,
1112
options: BalanceChangeOptions
1213
) {
13-
const subject = this._obj;
14-
const derivedPromise = Promise.all([
15-
getBalanceChange(subject, account, options),
16-
getAddressOf(account)
17-
]).then(
18-
([actualChange, address]) => {
19-
this.assert(
20-
actualChange.eq(BigNumber.from(balanceChange)),
21-
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
14+
transactionPromise(this);
15+
const isNegated = this.__flags.negate === true;
16+
const derivedPromise = this.txPromise.then(() => {
17+
return Promise.all([
18+
getBalanceChange(this.txResponse, account, options),
19+
getAddressOf(account)
20+
]);
21+
}).then(([actualChange, address]: [BigNumber, string]) => {
22+
const isCurrentlyNegated = this.__flags.negate === true;
23+
this.__flags.negate = isNegated;
24+
this.assert(
25+
actualChange.eq(BigNumber.from(balanceChange)),
26+
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
2227
`but it has changed by ${actualChange} wei`,
23-
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
24-
balanceChange,
25-
actualChange
26-
);
27-
}
28+
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
29+
balanceChange,
30+
actualChange
31+
);
32+
this.__flags.negate = isCurrentlyNegated;
33+
}
2834
);
2935
this.then = derivedPromise.then.bind(derivedPromise);
3036
this.catch = derivedPromise.catch.bind(derivedPromise);
31-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3238
return this;
3339
});
3440
}
3541

3642
export async function getBalanceChange(
37-
transaction:
38-
| providers.TransactionResponse
39-
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
43+
txResponse: providers.TransactionResponse,
4044
account: Account,
4145
options?: BalanceChangeOptions
4246
) {
4347
ensure(account.provider !== undefined, TypeError, 'Provider not found');
44-
45-
let txResponse: providers.TransactionResponse;
46-
47-
if (typeof transaction === 'function') {
48-
txResponse = await transaction();
49-
} else {
50-
txResponse = transaction;
51-
}
52-
5348
const txReceipt = await txResponse.wait();
5449
const txBlockNumber = txReceipt.blockNumber;
50+
const address = await getAddressOf(account);
5551

56-
const balanceAfter = await account.provider.getBalance(getAddressOf(account), txBlockNumber);
57-
const balanceBefore = await account.provider.getBalance(getAddressOf(account), txBlockNumber - 1);
52+
const balanceAfter = await account.provider.getBalance(address, txBlockNumber);
53+
const balanceBefore = await account.provider.getBalance(address, txBlockNumber - 1);
5854

59-
if (options?.includeFee !== true && await getAddressOf(account) === txResponse.from) {
55+
if (options?.includeFee !== true && address === txReceipt.from) {
6056
const gasPrice = txResponse.gasPrice ?? txReceipt.effectiveGasPrice;
6157
const gasUsed = txReceipt.gasUsed;
6258
const txFee = gasPrice.mul(gasUsed);

‎waffle-chai/src/matchers/changeEtherBalances.ts‎

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {getAddressOf, Account} from './misc/account';
34
import {BalanceChangeOptions, getAddresses, getBalances} from './misc/balance';
45

@@ -9,47 +10,40 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
910
balanceChanges: BigNumberish[],
1011
options: BalanceChangeOptions
1112
) {
12-
const subject = this._obj;
13-
14-
const derivedPromise = Promise.all([
15-
getBalanceChanges(subject, accounts, options),
16-
getAddresses(accounts)
17-
]).then(
18-
([actualChanges, accountAddresses]) => {
19-
this.assert(
20-
actualChanges.every((change, ind) =>
21-
change.eq(BigNumber.from(balanceChanges[ind]))
22-
),
23-
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
24-
`but it has changed by ${actualChanges} wei`,
25-
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
26-
balanceChanges.map((balanceChange) => balanceChange.toString()),
27-
actualChanges.map((actualChange) => actualChange.toString())
28-
);
29-
}
30-
);
13+
transactionPromise(this);
14+
const isNegated = this.__flags.negate === true;
15+
const derivedPromise = this.txPromise.then(() => {
16+
return Promise.all([
17+
getBalanceChanges(this.txResponse, accounts, options),
18+
getAddresses(accounts)
19+
]);
20+
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
21+
const isCurrentlyNegated = this.__flags.negate === true;
22+
this.__flags.negate = isNegated;
23+
this.assert(
24+
actualChanges.every((change, ind) =>
25+
change.eq(BigNumber.from(balanceChanges[ind]))
26+
),
27+
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
28+
`but it has changed by ${actualChanges} wei`,
29+
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
30+
balanceChanges.map((balanceChange) => balanceChange.toString()),
31+
actualChanges.map((actualChange) => actualChange.toString())
32+
);
33+
this.__flags.negate = isCurrentlyNegated;
34+
});
3135
this.then = derivedPromise.then.bind(derivedPromise);
3236
this.catch = derivedPromise.catch.bind(derivedPromise);
33-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3438
return this;
3539
});
3640
}
3741

3842
export async function getBalanceChanges(
39-
transaction:
40-
| providers.TransactionResponse
41-
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
43+
txResponse: providers.TransactionResponse,
4244
accounts: Account[],
4345
options: BalanceChangeOptions
4446
) {
45-
let txResponse: providers.TransactionResponse;
46-
47-
if (typeof transaction === 'function') {
48-
txResponse = await transaction();
49-
} else {
50-
txResponse = transaction;
51-
}
52-
5347
const txReceipt = await txResponse.wait();
5448
const txBlockNumber = txReceipt.blockNumber;
5549

0 commit comments

Comments
 (0)