Skip to content

Commit d3b63e7

Browse files
committed
Implement t.throws() and t.notThrows() ourselves
Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support for a validation function, which may or may not have worked. Refs #1047. `t.throws()` now fails if the exception is not an error. Fixes #1440. Regular expressions are now matched against the error message, not the result of casting the error to a string. Fixes #1445. Validate second argument to `t.throws()`. Refs #1676. Assertion failures now display how AVA arrived at the exception. Constructors are printed when the error is not a correct instance. Fixes #1471.
1 parent 5da97cc commit d3b63e7

File tree

7 files changed

+274
-126
lines changed

7 files changed

+274
-126
lines changed

index.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface ObservableLike {
22
subscribe(observer: (value: any) => void): void;
33
}
44

5-
export type ThrowsErrorValidator = (new (...args: Array<any>) => any) | RegExp | string | ((error: any) => boolean);
5+
export type ThrowsErrorValidator = (new (...args: Array<any>) => any) | RegExp | string;
66

77
export interface SnapshotOptions {
88
id?: string;

index.js.flow

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface ObservableLike {
77
subscribe(observer: (value: any) => void): void;
88
}
99

10-
export type ThrowsErrorValidator = Class<{constructor(...args: Array<any>): any}> | RegExp | string | ((error: any) => boolean);
10+
export type ThrowsErrorValidator = Class<{constructor(...args: Array<any>): any}> | RegExp | string;
1111

1212
export interface SnapshotOptions {
1313
id?: string;

lib/assert.js

+159-88
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22
const concordance = require('concordance');
3-
const coreAssert = require('core-assert');
43
const observableToPromise = require('observable-to-promise');
4+
const isError = require('is-error');
55
const isObservable = require('is-observable');
66
const isPromise = require('is-promise');
77
const concordanceOptions = require('./concordance-options').default;
@@ -74,9 +74,6 @@ function wrapAssertions(callbacks) {
7474
const fail = callbacks.fail;
7575

7676
const noop = () => {};
77-
const makeRethrow = reason => () => {
78-
throw reason;
79-
};
8077

8178
const assertions = {
8279
pass() {
@@ -160,154 +157,228 @@ function wrapAssertions(callbacks) {
160157
}
161158
},
162159

163-
throws(fn, err, message) {
164-
let promise;
165-
if (isPromise(fn)) {
166-
promise = fn;
167-
} else if (isObservable(fn)) {
168-
promise = observableToPromise(fn);
169-
} else if (typeof fn !== 'function') {
160+
throws(thrower, expected, message) {
161+
if (typeof thrower !== 'function' && !isPromise(thrower) && !isObservable(thrower)) {
170162
fail(this, new AssertionError({
171163
assertion: 'throws',
172164
improperUsage: true,
173-
message: '`t.throws()` must be called with a function, Promise, or Observable',
174-
values: [formatWithLabel('Called with:', fn)]
165+
message: '`t.throws()` must be called with a function, observable or promise',
166+
values: [formatWithLabel('Called with:', thrower)]
175167
}));
176168
return;
177169
}
178170

179-
let coreAssertThrowsErrorArg;
180-
if (typeof err === 'string') {
181-
const expectedMessage = err;
182-
coreAssertThrowsErrorArg = error => error.message === expectedMessage;
183-
} else {
184-
// Assume it's a constructor function or regular expression
185-
coreAssertThrowsErrorArg = err;
171+
if (typeof expected === 'function') {
172+
expected = {of: expected};
173+
} else if (typeof expected === 'string' || expected instanceof RegExp) {
174+
expected = {message: expected};
175+
} else if (arguments.length === 1) {
176+
expected = {};
177+
} else if (expected !== null) {
178+
fail(this, new AssertionError({
179+
assertion: 'throws',
180+
improperUsage: true,
181+
message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`',
182+
values: [formatWithLabel('Called with:', expected)]
183+
}));
184+
return;
186185
}
187186

188-
let maybePromise;
189-
const test = (fn, stack) => {
190-
let actual;
191-
let threw = false;
192-
try {
193-
coreAssert.throws(() => {
194-
try {
195-
maybePromise = fn();
196-
} catch (err) {
197-
actual = err;
198-
threw = true;
199-
throw err;
200-
}
201-
}, coreAssertThrowsErrorArg);
202-
return actual;
203-
} catch (err) {
187+
// Note: this function *must* throw exceptions, since it can be used
188+
// as part of a pending assertion for observables and promises.
189+
const assertExpected = (actual, prefix, stack) => {
190+
if (!isError(actual)) {
191+
throw new AssertionError({
192+
assertion: 'throws',
193+
message,
194+
stack,
195+
values: [formatWithLabel(`${prefix} exception that is not an error:`, actual)]
196+
});
197+
}
198+
199+
if (expected.of && !(actual instanceof expected.of)) {
200+
throw new AssertionError({
201+
assertion: 'throws',
202+
message,
203+
stack,
204+
values: [
205+
formatWithLabel(`${prefix} unexpected exception:`, actual),
206+
formatWithLabel('Expected instance of:', expected.of)
207+
]
208+
});
209+
}
210+
211+
if (typeof expected.message === 'string' && actual.message !== expected.message) {
204212
throw new AssertionError({
205213
assertion: 'throws',
206214
message,
207215
stack,
208-
values: threw ?
209-
[formatWithLabel('Threw unexpected exception:', actual)] :
210-
null
216+
values: [
217+
formatWithLabel(`${prefix} unexpected exception:`, actual),
218+
formatWithLabel('Expected message to equal:', expected.message)
219+
]
211220
});
212221
}
222+
223+
if (expected.message instanceof RegExp && !expected.message.test(actual.message)) {
224+
throw new AssertionError({
225+
assertion: 'throws',
226+
message,
227+
stack,
228+
values: [
229+
formatWithLabel(`${prefix} unexpected exception:`, actual),
230+
formatWithLabel('Expected message to match:', expected.message)
231+
]
232+
});
233+
}
234+
};
235+
236+
const handleObservable = (observable, wasReturned) => {
237+
// Record stack before it gets lost in the promise chain.
238+
const stack = getStack();
239+
const intermediate = observableToPromise(observable).then(value => {
240+
throw new AssertionError({
241+
assertion: 'throws',
242+
message,
243+
stack,
244+
values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} completed with:`, value)]
245+
});
246+
}, reason => {
247+
assertExpected(reason, `${wasReturned ? 'Returned observable' : 'Observable'} errored with`, stack);
248+
return reason;
249+
});
250+
251+
pending(this, intermediate);
252+
// Don't reject the returned promise, even if the assertion fails.
253+
return intermediate.catch(noop);
213254
};
214255

215-
const handlePromise = promise => {
256+
const handlePromise = (promise, wasReturned) => {
216257
// Record stack before it gets lost in the promise chain.
217258
const stack = getStack();
218259
const intermediate = promise.then(value => {
219260
throw new AssertionError({
220261
assertion: 'throws',
221-
message: 'Expected promise to be rejected, but it was resolved instead',
222-
values: [formatWithLabel('Resolved with:', value)]
262+
message,
263+
stack,
264+
values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} resolved with:`, value)]
223265
});
224-
}, reason => test(makeRethrow(reason), stack));
266+
}, reason => {
267+
assertExpected(reason, `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, stack);
268+
return reason;
269+
});
225270

226271
pending(this, intermediate);
227272
// Don't reject the returned promise, even if the assertion fails.
228273
return intermediate.catch(noop);
229274
};
230275

231-
if (promise) {
232-
return handlePromise(promise);
276+
if (isPromise(thrower)) {
277+
return handlePromise(thrower, false);
278+
} else if (isObservable(thrower)) {
279+
return handleObservable(thrower, false);
233280
}
234281

282+
let retval;
283+
let actual;
284+
let threw = false;
235285
try {
236-
const retval = test(fn);
237-
pass(this);
238-
return retval;
286+
retval = thrower();
239287
} catch (err) {
240-
if (maybePromise) {
241-
if (isPromise(maybePromise)) {
242-
return handlePromise(maybePromise);
243-
}
244-
if (isObservable(maybePromise)) {
245-
return handlePromise(observableToPromise(maybePromise));
246-
}
288+
actual = err;
289+
threw = true;
290+
}
291+
292+
if (!threw) {
293+
if (isPromise(retval)) {
294+
return handlePromise(retval, true);
295+
} else if (isObservable(retval)) {
296+
return handleObservable(retval, true);
247297
}
298+
fail(this, new AssertionError({
299+
assertion: 'throws',
300+
message,
301+
values: [formatWithLabel('Function returned:', retval)]
302+
}));
303+
return;
304+
}
305+
306+
try {
307+
assertExpected(actual, 'Function threw');
308+
pass(this);
309+
return actual;
310+
} catch (err) {
248311
fail(this, err);
249312
}
250313
},
251314

252-
notThrows(fn, message) {
253-
let promise;
254-
if (isPromise(fn)) {
255-
promise = fn;
256-
} else if (isObservable(fn)) {
257-
promise = observableToPromise(fn);
258-
} else if (typeof fn !== 'function') {
315+
notThrows(nonThrower, message) {
316+
if (typeof nonThrower !== 'function' && !isPromise(nonThrower) && !isObservable(nonThrower)) {
259317
fail(this, new AssertionError({
260318
assertion: 'notThrows',
261319
improperUsage: true,
262-
message: '`t.notThrows()` must be called with a function, Promise, or Observable',
263-
values: [formatWithLabel('Called with:', fn)]
320+
message: '`t.notThrows()` must be called with a function, observable or promise',
321+
values: [formatWithLabel('Called with:', nonThrower)]
264322
}));
265323
return;
266324
}
267325

268-
let maybePromise;
269-
const test = (fn, stack) => {
270-
try {
271-
coreAssert.doesNotThrow(() => {
272-
maybePromise = fn();
273-
});
274-
} catch (err) {
326+
const handleObservable = (observable, wasReturned) => {
327+
// Record stack before it gets lost in the promise chain.
328+
const stack = getStack();
329+
const intermediate = observableToPromise(observable).then(noop, reason => {
275330
throw new AssertionError({
276331
assertion: 'notThrows',
277332
message,
278333
stack,
279-
values: [formatWithLabel('Threw:', err.actual)]
334+
values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} errored with:`, reason)]
280335
});
281-
}
336+
});
337+
pending(this, intermediate);
338+
// Don't reject the returned promise, even if the assertion fails.
339+
return intermediate.catch(noop);
282340
};
283341

284-
const handlePromise = promise => {
342+
const handlePromise = (promise, wasReturned) => {
285343
// Record stack before it gets lost in the promise chain.
286344
const stack = getStack();
287-
const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack));
345+
const intermediate = promise.then(noop, reason => {
346+
throw new AssertionError({
347+
assertion: 'notThrows',
348+
message,
349+
stack,
350+
values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, reason)]
351+
});
352+
});
288353
pending(this, intermediate);
289354
// Don't reject the returned promise, even if the assertion fails.
290355
return intermediate.catch(noop);
291356
};
292357

293-
if (promise) {
294-
return handlePromise(promise);
358+
if (isPromise(nonThrower)) {
359+
return handlePromise(nonThrower, false);
360+
} else if (isObservable(nonThrower)) {
361+
return handleObservable(nonThrower, false);
295362
}
296363

364+
let retval;
297365
try {
298-
test(fn);
299-
if (maybePromise) {
300-
if (isPromise(maybePromise)) {
301-
return handlePromise(maybePromise);
302-
}
303-
if (isObservable(maybePromise)) {
304-
return handlePromise(observableToPromise(maybePromise));
305-
}
306-
}
307-
pass(this);
366+
retval = nonThrower();
308367
} catch (err) {
309-
fail(this, err);
368+
fail(this, new AssertionError({
369+
assertion: 'notThrows',
370+
message,
371+
values: [formatWithLabel(`Function threw:`, err)]
372+
}));
373+
return;
310374
}
375+
376+
if (isPromise(retval)) {
377+
return handlePromise(retval, true);
378+
} else if (isObservable(retval)) {
379+
return handleObservable(retval, true);
380+
}
381+
pass(this);
311382
},
312383

313384
ifError(actual, message) {

package-lock.json

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
"common-path-prefix": "^1.0.0",
9191
"concordance": "^3.0.0",
9292
"convert-source-map": "^1.5.1",
93-
"core-assert": "^0.2.0",
9493
"currently-unhandled": "^0.4.1",
9594
"debug": "^3.1.0",
9695
"dot-prop": "^4.2.0",
@@ -104,6 +103,7 @@
104103
"import-local": "^1.0.0",
105104
"indent-string": "^3.2.0",
106105
"is-ci": "^1.1.0",
106+
"is-error": "^2.2.1",
107107
"is-generator-fn": "^1.0.0",
108108
"is-obj": "^1.0.0",
109109
"is-observable": "^1.1.0",

0 commit comments

Comments
 (0)