|
1 | 1 | 'use strict';
|
2 | 2 | const concordance = require('concordance');
|
3 |
| -const coreAssert = require('core-assert'); |
4 | 3 | const observableToPromise = require('observable-to-promise');
|
| 4 | +const isError = require('is-error'); |
5 | 5 | const isObservable = require('is-observable');
|
6 | 6 | const isPromise = require('is-promise');
|
7 | 7 | const concordanceOptions = require('./concordance-options').default;
|
@@ -74,9 +74,6 @@ function wrapAssertions(callbacks) {
|
74 | 74 | const fail = callbacks.fail;
|
75 | 75 |
|
76 | 76 | const noop = () => {};
|
77 |
| - const makeRethrow = reason => () => { |
78 |
| - throw reason; |
79 |
| - }; |
80 | 77 |
|
81 | 78 | const assertions = {
|
82 | 79 | pass() {
|
@@ -160,154 +157,228 @@ function wrapAssertions(callbacks) {
|
160 | 157 | }
|
161 | 158 | },
|
162 | 159 |
|
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)) { |
170 | 162 | fail(this, new AssertionError({
|
171 | 163 | assertion: 'throws',
|
172 | 164 | 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)] |
175 | 167 | }));
|
176 | 168 | return;
|
177 | 169 | }
|
178 | 170 |
|
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; |
186 | 185 | }
|
187 | 186 |
|
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) { |
204 | 212 | throw new AssertionError({
|
205 | 213 | assertion: 'throws',
|
206 | 214 | message,
|
207 | 215 | 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 | + ] |
211 | 220 | });
|
212 | 221 | }
|
| 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); |
213 | 254 | };
|
214 | 255 |
|
215 |
| - const handlePromise = promise => { |
| 256 | + const handlePromise = (promise, wasReturned) => { |
216 | 257 | // Record stack before it gets lost in the promise chain.
|
217 | 258 | const stack = getStack();
|
218 | 259 | const intermediate = promise.then(value => {
|
219 | 260 | throw new AssertionError({
|
220 | 261 | 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)] |
223 | 265 | });
|
224 |
| - }, reason => test(makeRethrow(reason), stack)); |
| 266 | + }, reason => { |
| 267 | + assertExpected(reason, `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, stack); |
| 268 | + return reason; |
| 269 | + }); |
225 | 270 |
|
226 | 271 | pending(this, intermediate);
|
227 | 272 | // Don't reject the returned promise, even if the assertion fails.
|
228 | 273 | return intermediate.catch(noop);
|
229 | 274 | };
|
230 | 275 |
|
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); |
233 | 280 | }
|
234 | 281 |
|
| 282 | + let retval; |
| 283 | + let actual; |
| 284 | + let threw = false; |
235 | 285 | try {
|
236 |
| - const retval = test(fn); |
237 |
| - pass(this); |
238 |
| - return retval; |
| 286 | + retval = thrower(); |
239 | 287 | } 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); |
247 | 297 | }
|
| 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) { |
248 | 311 | fail(this, err);
|
249 | 312 | }
|
250 | 313 | },
|
251 | 314 |
|
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)) { |
259 | 317 | fail(this, new AssertionError({
|
260 | 318 | assertion: 'notThrows',
|
261 | 319 | 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)] |
264 | 322 | }));
|
265 | 323 | return;
|
266 | 324 | }
|
267 | 325 |
|
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 => { |
275 | 330 | throw new AssertionError({
|
276 | 331 | assertion: 'notThrows',
|
277 | 332 | message,
|
278 | 333 | stack,
|
279 |
| - values: [formatWithLabel('Threw:', err.actual)] |
| 334 | + values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} errored with:`, reason)] |
280 | 335 | });
|
281 |
| - } |
| 336 | + }); |
| 337 | + pending(this, intermediate); |
| 338 | + // Don't reject the returned promise, even if the assertion fails. |
| 339 | + return intermediate.catch(noop); |
282 | 340 | };
|
283 | 341 |
|
284 |
| - const handlePromise = promise => { |
| 342 | + const handlePromise = (promise, wasReturned) => { |
285 | 343 | // Record stack before it gets lost in the promise chain.
|
286 | 344 | 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 | + }); |
288 | 353 | pending(this, intermediate);
|
289 | 354 | // Don't reject the returned promise, even if the assertion fails.
|
290 | 355 | return intermediate.catch(noop);
|
291 | 356 | };
|
292 | 357 |
|
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); |
295 | 362 | }
|
296 | 363 |
|
| 364 | + let retval; |
297 | 365 | 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(); |
308 | 367 | } 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; |
310 | 374 | }
|
| 375 | + |
| 376 | + if (isPromise(retval)) { |
| 377 | + return handlePromise(retval, true); |
| 378 | + } else if (isObservable(retval)) { |
| 379 | + return handleObservable(retval, true); |
| 380 | + } |
| 381 | + pass(this); |
311 | 382 | },
|
312 | 383 |
|
313 | 384 | ifError(actual, message) {
|
|
0 commit comments