Make the chai helper for rich reverts in dev-utils more robust.
Add rich reverts chai helper tests to `dev-utils`
This commit is contained in:
committed by
Amir Bandeali
parent
ff1a3ab307
commit
ed78bde359
@@ -49,6 +49,9 @@
|
||||
"@0x/web3-wrapper": "^6.0.7",
|
||||
"@types/web3-provider-engine": "^14.0.0",
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-bignumber": "^3.0.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"ethereum-types": "^2.1.3",
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RichRevertReason } from '@0x/utils';
|
||||
import { RichRevertReason, StandardError } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
import ChaiBigNumber = require('chai-bignumber');
|
||||
@@ -7,44 +7,116 @@ import * as dirtyChai from 'dirty-chai';
|
||||
export const chaiSetup = {
|
||||
configure(): void {
|
||||
chai.config.includeStack = true;
|
||||
// Order matters.
|
||||
chai.use(ChaiBigNumber());
|
||||
chai.use(dirtyChai);
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(richRevertEquality);
|
||||
chai.use(richRevertExtension);
|
||||
chai.use(dirtyChai);
|
||||
},
|
||||
};
|
||||
|
||||
function richRevertEquality(_chai: any): void {
|
||||
// tslint:disable:only-arrow-functions
|
||||
const Assertion = _chai.Assertion;
|
||||
Assertion.overwriteMethod('rejectedWith', function(_super: (x: any) => Promise<void>): (b: any) => Promise<void> {
|
||||
return async function(this: any, b: any | RichRevertReason): Promise<void> {
|
||||
const p = this._obj;
|
||||
let a;
|
||||
// tslint:disable: only-arrow-functions prefer-conditional-expression
|
||||
|
||||
type ChaiPromiseHandler = (x: any) => Promise<void>;
|
||||
type ChaiAssertHandler = (x: any) => void;
|
||||
|
||||
interface Chai {
|
||||
Assertion: any;
|
||||
}
|
||||
|
||||
interface ChaiUtils {
|
||||
flag: (assertion: any, name: string, value?: any) => any;
|
||||
overwriteMethod: (ctx: any, name: string, _super: (expected: any) => any) => void;
|
||||
}
|
||||
|
||||
interface ChaiExtension {
|
||||
assert: ChaiAssert;
|
||||
_obj: any;
|
||||
__flags: any;
|
||||
}
|
||||
|
||||
type ChaiAssert = (
|
||||
condition: boolean,
|
||||
failMessage?: string,
|
||||
negatedFailMessage?: string,
|
||||
expected?: any,
|
||||
actual?: any,
|
||||
) => void;
|
||||
|
||||
function richRevertExtension(_chai: Chai, utils: ChaiUtils): void {
|
||||
utils.overwriteMethod(_chai.Assertion.prototype, 'rejectedWith', function(
|
||||
_super: ChaiPromiseHandler,
|
||||
): ChaiPromiseHandler {
|
||||
return async function(this: ChaiExtension, expected: any): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to reject.
|
||||
let err: any;
|
||||
let didReject = false;
|
||||
try {
|
||||
if (p instanceof Promise) {
|
||||
await p;
|
||||
this.assert(false, 'Expected promise to reject');
|
||||
} else {
|
||||
a = p;
|
||||
}
|
||||
} catch (err) {
|
||||
a = err;
|
||||
await maybePromise;
|
||||
} catch (_err) {
|
||||
err = _err;
|
||||
didReject = true;
|
||||
}
|
||||
if (b instanceof RichRevertReason) {
|
||||
// Try to decode the result as a rich revert reason.
|
||||
if (a instanceof Buffer) {
|
||||
const hex = a.toString('hex');
|
||||
a = `0x${hex}`;
|
||||
}
|
||||
if (typeof a === 'string') {
|
||||
a = RichRevertReason.decode(a);
|
||||
}
|
||||
if (a instanceof RichRevertReason) {
|
||||
this.assert(a.equals(b), `${a} != ${b}`);
|
||||
}
|
||||
if (!didReject) {
|
||||
new _chai.Assertion().assert(false, `Expected promise to reject`);
|
||||
}
|
||||
_super.call(this, b);
|
||||
return compareRichRevertReasons.call(this, _chai, _super, err, expected);
|
||||
};
|
||||
});
|
||||
utils.overwriteMethod(_chai.Assertion.prototype, 'become', function(
|
||||
_super: ChaiPromiseHandler,
|
||||
): ChaiPromiseHandler {
|
||||
return async function(this: ChaiExtension, expected: any): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to resolve.
|
||||
return compareRichRevertReasons.call(this, _chai, _super, await maybePromise, expected);
|
||||
};
|
||||
});
|
||||
utils.overwriteMethod(_chai.Assertion.prototype, 'equal', function(_super: ChaiAssertHandler): ChaiAssertHandler {
|
||||
return function(this: ChaiExtension, expected: any): void {
|
||||
compareRichRevertReasons.call(this, _chai, _super, this._obj, expected);
|
||||
};
|
||||
});
|
||||
utils.overwriteMethod(_chai.Assertion.prototype, 'eql', function(_super: ChaiAssertHandler): ChaiAssertHandler {
|
||||
return function(this: ChaiExtension, expected: any): void {
|
||||
compareRichRevertReasons.call(this, _chai, _super, this._obj, expected);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function compareRichRevertReasons(
|
||||
this: ChaiExtension,
|
||||
_chai: Chai,
|
||||
_super: ChaiAssertHandler,
|
||||
_actual: any,
|
||||
_expected: any,
|
||||
): void {
|
||||
let actual = _actual;
|
||||
let expected = _expected;
|
||||
// If either subject is a StandardError, try to coerce the other into the same.
|
||||
if (expected instanceof StandardError || actual instanceof StandardError) {
|
||||
// `actual` can be a RichRevertReason, string, or an Error type.
|
||||
if (typeof actual === 'string') {
|
||||
actual = new StandardError(actual);
|
||||
} else if (actual instanceof Error) {
|
||||
actual = new StandardError(actual.message);
|
||||
} else if (!(actual instanceof RichRevertReason)) {
|
||||
new _chai.Assertion().assert(false, `Result is not of type RichRevertReason: ${actual}`);
|
||||
}
|
||||
// `expected` can be a RichRevertReason or string.
|
||||
if (typeof expected === 'string') {
|
||||
expected = new StandardError(expected);
|
||||
}
|
||||
}
|
||||
if (expected instanceof RichRevertReason && actual instanceof RichRevertReason) {
|
||||
// Check for equality.
|
||||
this.assert(actual.equals(expected), `${actual} != ${expected}`, `${actual} == ${expected}`, expected, actual);
|
||||
return;
|
||||
}
|
||||
_super.call(this, _expected);
|
||||
}
|
||||
|
||||
121
packages/dev-utils/test/chai_test.ts
Normal file
121
packages/dev-utils/test/chai_test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { StandardError } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
|
||||
import { chaiSetup } from '../src';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
class CustomError extends StandardError {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Chai tests', () => {
|
||||
describe('RichRevertReasons', () => {
|
||||
describe('#equal', () => {
|
||||
it('should equate two identical RichRevertReasons', () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new StandardError(message);
|
||||
expect(reason1).is.equal(reason2);
|
||||
});
|
||||
it('should equate two RichRevertReasons where one has missing fields', () => {
|
||||
const reason1 = new StandardError('foo');
|
||||
const reason2 = new StandardError();
|
||||
expect(reason1).is.equal(reason2);
|
||||
});
|
||||
it('should not equate two RichRevertReasons with diferent fields', () => {
|
||||
const reason1 = new StandardError('foo1');
|
||||
const reason2 = new StandardError('foo2');
|
||||
expect(reason1).is.not.equal(reason2);
|
||||
});
|
||||
it('should not equate two RichRevertReasons with diferent types', () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new CustomError(message);
|
||||
expect(reason1).is.not.equal(reason2);
|
||||
});
|
||||
it('should equate a StandardError to a string equal to message', () => {
|
||||
const message = 'foo';
|
||||
const reason = new StandardError(message);
|
||||
expect(message).is.equal(reason);
|
||||
});
|
||||
it('should equate an Error to a StandardError with an equal message', () => {
|
||||
const message = 'foo';
|
||||
const reason = new StandardError(message);
|
||||
const error = new Error(message);
|
||||
expect(error).is.equal(reason);
|
||||
});
|
||||
it('should equate a string to a StandardError with the same message', () => {
|
||||
const message = 'foo';
|
||||
const reason = new StandardError(message);
|
||||
expect(reason).is.equal(message);
|
||||
});
|
||||
it('should not equate a StandardError to a string not equal to message', () => {
|
||||
const reason = new StandardError('foo1');
|
||||
expect('foo2').is.not.equal(reason);
|
||||
});
|
||||
it('should not equate a string to a StandardError with a different message', () => {
|
||||
const reason = new StandardError('foo1');
|
||||
expect(reason).is.not.equal('foo2');
|
||||
});
|
||||
it('should not equate an Error to a StandardError with a different message', () => {
|
||||
const reason = new StandardError('foo1');
|
||||
const error = new Error('foo2');
|
||||
expect(error).is.not.equal(reason);
|
||||
});
|
||||
});
|
||||
describe('#rejectedWith', () => {
|
||||
it('should equate a promise that rejects to an identical RichRevertReasons', async () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new StandardError(message);
|
||||
const promise = (async () => {
|
||||
throw reason1;
|
||||
})();
|
||||
return expect(promise).to.be.rejectedWith(reason2);
|
||||
});
|
||||
it('should not equate a promise that rejects to a StandardError with a different messages', async () => {
|
||||
const reason1 = new StandardError('foo1');
|
||||
const reason2 = new StandardError('foo2');
|
||||
const promise = (async () => {
|
||||
throw reason1;
|
||||
})();
|
||||
return expect(promise).to.not.be.rejectedWith(reason2);
|
||||
});
|
||||
it('should not equate a promise that rejects to different RichRevertReason types', async () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new CustomError(message);
|
||||
const promise = (async () => {
|
||||
throw reason1;
|
||||
})();
|
||||
return expect(promise).to.not.be.rejectedWith(reason2);
|
||||
});
|
||||
});
|
||||
describe('#become', () => {
|
||||
it('should equate a promise that resolves to an identical RichRevertReasons', async () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new StandardError(message);
|
||||
const promise = (async () => reason1)();
|
||||
return expect(promise).to.become(reason2);
|
||||
});
|
||||
it('should not equate a promise that resolves to a StandardError with a different messages', async () => {
|
||||
const reason1 = new StandardError('foo1');
|
||||
const reason2 = new StandardError('foo2');
|
||||
const promise = (async () => reason1)();
|
||||
return expect(promise).to.not.become(reason2);
|
||||
});
|
||||
it('should not equate a promise that resolves to different RichRevertReason types', async () => {
|
||||
const message = 'foo';
|
||||
const reason1 = new StandardError(message);
|
||||
const reason2 = new CustomError(message);
|
||||
const promise = (async () => reason1)();
|
||||
return expect(promise).to.not.become(reason2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user