In @0x/dev-utils fix the RevertError chai helper's equal override not passing all arguments to previous handler.

In `@0x/dev-utils` add more `RevertError` chai helper tests for backwards compatibility with `rejectedWith`.
In `@0x/dev-utils` instead of overriding `rejectedWith`, add a new method `revertWith`.
In `@0x/dev-utils` clean up the code for the `RevertError` chai helper.
This commit is contained in:
Lawrence Forman 2019-04-04 09:53:46 -04:00 committed by Amir Bandeali
parent 6583ac9ba1
commit e00ac37cb2
3 changed files with 105 additions and 51 deletions

View File

@ -10,31 +10,31 @@ export const chaiSetup = {
// Order matters. // Order matters.
chai.use(ChaiBigNumber()); chai.use(ChaiBigNumber());
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
chai.use(richRevertExtension); chai.use(revertErrorHelper);
chai.use(dirtyChai); chai.use(dirtyChai);
}, },
}; };
// tslint:disable: only-arrow-functions prefer-conditional-expression // tslint:disable: only-arrow-functions prefer-conditional-expression
type ChaiPromiseHandler = (x: any) => Promise<void>; type ChaiPromiseHandler = (x: any, ...rest: any[]) => Promise<void>;
type ChaiAssertHandler = (x: any) => void; type ChaiAssertHandler = (x: any, ...rest: any[]) => void;
interface Chai { interface Chai {
Assertion: any; Assertion: ChaiAssertionType;
} }
interface ChaiUtils { interface ChaiAssertionInstance {
flag: (assertion: any, name: string, value?: any) => any;
overwriteMethod: (ctx: any, name: string, _super: (expected: any) => any) => void;
}
interface ChaiExtension {
assert: ChaiAssert; assert: ChaiAssert;
_obj: any; _obj: any;
__flags: any; __flags: any;
} }
interface ChaiAssertionType {
overwriteMethod: (name: string, _super: (expected: any) => any) => void;
new (): ChaiAssertionInstance;
}
type ChaiAssert = ( type ChaiAssert = (
condition: boolean, condition: boolean,
failMessage?: string, failMessage?: string,
@ -43,71 +43,77 @@ type ChaiAssert = (
actual?: any, actual?: any,
) => void; ) => void;
function richRevertExtension(_chai: Chai, utils: ChaiUtils): void { function revertErrorHelper(_chai: Chai): void {
utils.overwriteMethod(_chai.Assertion.prototype, 'rejectedWith', function( const proto = _chai.Assertion;
_super: ChaiPromiseHandler, proto.overwriteMethod('revertWith', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
): ChaiPromiseHandler { return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
return async function(this: ChaiExtension, expected: any): Promise<void> {
const maybePromise = this._obj; const maybePromise = this._obj;
// Make sure we're working with a promise. // Make sure we're working with a promise.
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`); chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
// Wait for the promise to reject. // Wait for the promise to reject.
let err: any; let resolveValue;
let rejectValue: any;
let didReject = false; let didReject = false;
try { try {
await maybePromise; resolveValue = await maybePromise;
} catch (_err) { } catch (err) {
err = _err; rejectValue = err;
didReject = true; didReject = true;
} }
if (!didReject) { if (!didReject) {
new _chai.Assertion().assert(false, `Expected promise to reject`); chaiFail(_chai, `Expected promise to reject but instead resolved with: ${resolveValue}`);
}
if (!compareRevertErrors.call(this, _chai, rejectValue, expected)) {
// Wasn't handled by the comparison function so call the previous handler.
_super.call(this, expected, ...rest);
} }
return compareRichRevertReasons.call(this, _chai, _super, err, expected);
}; };
}); });
utils.overwriteMethod(_chai.Assertion.prototype, 'become', function( proto.overwriteMethod('become', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
_super: ChaiPromiseHandler, return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
): ChaiPromiseHandler {
return async function(this: ChaiExtension, expected: any): Promise<void> {
const maybePromise = this._obj; const maybePromise = this._obj;
// Make sure we're working with a promise. // Make sure we're working with a promise.
new _chai.Assertion().assert(maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`); chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
// Wait for the promise to resolve. // Wait for the promise to resolve.
return compareRichRevertReasons.call(this, _chai, _super, await maybePromise, expected); if (!compareRevertErrors.call(this, _chai, await maybePromise, expected)) {
// Wasn't handled by the comparison function so call the previous handler.
_super.call(this, expected, ...rest);
}
}; };
}); });
utils.overwriteMethod(_chai.Assertion.prototype, 'equal', function(_super: ChaiAssertHandler): ChaiAssertHandler { proto.overwriteMethod('equal', function(_super: ChaiAssertHandler): ChaiAssertHandler {
return function(this: ChaiExtension, expected: any): void { return function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): void {
compareRichRevertReasons.call(this, _chai, _super, this._obj, expected); if (!compareRevertErrors.call(this, _chai, this._obj, expected)) {
}; // Wasn't handled by the comparison function so call the previous handler.
}); _super.call(this, expected, ...rest);
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, * Compare two values as compatible RevertError types.
_chai: Chai, * @return `true` if the comparison was fully evaluated. `false` indicates that
_super: ChaiAssertHandler, * it should be deferred to another handler.
_actual: any, */
_expected: any, function compareRevertErrors(this: ChaiAssertionInstance, _chai: Chai, _actual: any, _expected: any): boolean {
): void {
let actual = _actual; let actual = _actual;
let expected = _expected; let expected = _expected;
// If either subject is a RevertError, try to coerce the other into the same. // If either subject is a RevertError, try to coerce the other into the same.
// Some of this is for convenience, some is for backwards-compatibility.
// TODO: Remove coercion of `actual` when all contracts and tests are upgraded
// to explicitly use RevertErrors.
if (expected instanceof RevertError || actual instanceof RevertError) { if (expected instanceof RevertError || actual instanceof RevertError) {
// `actual` can be a RevertError, string, or an Error type. // `actual` can be a RevertError, string, or an Error type.
if (!(actual instanceof RevertError)) { if (!(actual instanceof RevertError)) {
if (typeof actual === 'string') { if (typeof actual === 'string') {
actual = new StringRevertError(actual); actual = new StringRevertError(actual);
} else if (actual instanceof Error) { } else if (actual instanceof Error) {
// `BaseContract` will throw a plain `Error` type for `StringRevertErrors`
// for backwards compatibility. So coerce it into a StringRevertError.
actual = new StringRevertError(actual.message); actual = new StringRevertError(actual.message);
} else { } else {
new _chai.Assertion().assert(false, `Result is not of type RevertError: ${actual}`); chaiAssert(_chai, false, `Result is not of type RevertError: ${actual}`);
} }
} }
// `expected` can be a RevertError or string. // `expected` can be a RevertError or string.
@ -124,7 +130,18 @@ function compareRichRevertReasons(
expected, expected,
actual, actual,
); );
return; // Return true to signal we handled it.
return true;
} }
_super.call(this, _expected); return false;
}
function chaiAssert(_chai: Chai, condition: boolean, failMessage?: string, expected?: any, actual?: any): void {
const assert = new _chai.Assertion();
assert.assert(condition, failMessage, undefined, expected, actual);
}
function chaiFail(_chai: Chai, failMessage?: string, expected?: any, actual?: any): void {
const assert = new _chai.Assertion();
assert.assert(false, failMessage, undefined, expected, actual);
} }

View File

@ -4,3 +4,13 @@ declare module '*.json' {
export default json; export default json;
/* tslint:enable */ /* tslint:enable */
} }
declare module 'chai' {
global {
export namespace Chai {
export interface Assertion {
revertWith: (expected: string | RevertError) => Promise<void>;
}
}
}
}

View File

@ -67,15 +67,15 @@ describe('Chai tests', () => {
expect(error).is.not.equal(revert); expect(error).is.not.equal(revert);
}); });
}); });
describe('#rejectedWith', () => { describe('#revertWith', () => {
it('should equate a promise that rejects to an identical RevertErrors', async () => { it('should equate a promise that rejects to identical RevertErrors', async () => {
const message = 'foo'; const message = 'foo';
const revert1 = new StringRevertError(message); const revert1 = new StringRevertError(message);
const revert2 = new StringRevertError(message); const revert2 = new StringRevertError(message);
const promise = (async () => { const promise = (async () => {
throw revert1; throw revert1;
})(); })();
return expect(promise).to.be.rejectedWith(revert2); return expect(promise).to.revertWith(revert2);
}); });
it('should not equate a promise that rejects to a StringRevertError with a different messages', async () => { it('should not equate a promise that rejects to a StringRevertError with a different messages', async () => {
const revert1 = new StringRevertError('foo1'); const revert1 = new StringRevertError('foo1');
@ -83,7 +83,7 @@ describe('Chai tests', () => {
const promise = (async () => { const promise = (async () => {
throw revert1; throw revert1;
})(); })();
return expect(promise).to.not.be.rejectedWith(revert2); return expect(promise).to.not.revertWith(revert2);
}); });
it('should not equate a promise that rejects to different RevertError types', async () => { it('should not equate a promise that rejects to different RevertError types', async () => {
const message = 'foo'; const message = 'foo';
@ -92,7 +92,7 @@ describe('Chai tests', () => {
const promise = (async () => { const promise = (async () => {
throw revert1; throw revert1;
})(); })();
return expect(promise).to.not.be.rejectedWith(revert2); return expect(promise).to.not.revertWith(revert2);
}); });
}); });
describe('#become', () => { describe('#become', () => {
@ -117,5 +117,32 @@ describe('Chai tests', () => {
return expect(promise).to.not.become(revert2); return expect(promise).to.not.become(revert2);
}); });
}); });
// TODO: Remove these tests when we no longer coerce `Error` types to `StringRevertError` types
// for backwards compatibility.
describe('#rejectedWith (backwards compatibility)', () => {
it('should equate a promise that rejects with an Error to a string of the same message', async () => {
const message = 'foo';
const revert1 = new Error(message);
const promise = (async () => {
throw revert1;
})();
return expect(promise).to.rejectedWith(message);
});
it('should equate a promise that rejects with an StringRevertErrors to a string of the same message', async () => {
const message = 'foo';
const revert = new StringRevertError(message);
const promise = (async () => {
throw revert;
})();
return expect(promise).to.rejectedWith(message);
});
it('should not equate a promise that rejects with an StringRevertErrors to a string with different messages', async () => {
const revert = new StringRevertError('foo1');
const promise = (async () => {
throw revert;
})();
return expect(promise).to.not.be.rejectedWith('foo2');
});
});
}); });
}); });