-
Notifications
You must be signed in to change notification settings - Fork 108
/
chai-as-promised.js
453 lines (387 loc) · 14.1 KB
/
chai-as-promised.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
import * as checkErrorDefault from 'check-error';
let checkError = checkErrorDefault;
export default function (chai, utils) {
const Assertion = chai.Assertion;
const assert = chai.assert;
const proxify = utils.proxify;
// If we are using a version of Chai that has checkError on it,
// we want to use that version to be consistent. Otherwise, we use
// what was passed to the factory.
if (utils.checkError) {
checkError = utils.checkError;
}
function isLegacyJQueryPromise(thenable) {
// jQuery promises are Promises/A+-compatible since 3.0.0. jQuery 3.0.0 is also the first version
// to define the catch method.
return (
typeof thenable.catch !== 'function' &&
typeof thenable.always === 'function' &&
typeof thenable.done === 'function' &&
typeof thenable.fail === 'function' &&
typeof thenable.pipe === 'function' &&
typeof thenable.progress === 'function' &&
typeof thenable.state === 'function'
);
}
function assertIsAboutPromise(assertion) {
if (typeof assertion._obj.then !== 'function') {
throw new TypeError(
utils.inspect(assertion._obj) + ' is not a thenable.'
);
}
if (isLegacyJQueryPromise(assertion._obj)) {
throw new TypeError(
'Chai as Promised is incompatible with thenables of jQuery<3.0.0, sorry! Please ' +
'upgrade jQuery or use another Promises/A+ compatible library (see ' +
'http://promisesaplus.com/).'
);
}
}
function proxifyIfSupported(assertion) {
return proxify === undefined ? assertion : proxify(assertion);
}
function method(name, asserter) {
utils.addMethod(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return asserter.apply(this, arguments);
});
}
function property(name, asserter) {
utils.addProperty(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return proxifyIfSupported(asserter.apply(this, arguments));
});
}
function doNotify(promise, done) {
promise.then(() => done(), done);
}
// These are for clarity and to bypass Chai refusing to allow `undefined` as actual when used with `assert`.
function assertIfNegated(assertion, message, extra) {
assertion.assert(true, null, message, extra.expected, extra.actual);
}
function assertIfNotNegated(assertion, message, extra) {
assertion.assert(false, message, null, extra.expected, extra.actual);
}
function getBasePromise(assertion) {
// We need to chain subsequent asserters on top of ones in the chain already (consider
// `eventually.have.property("foo").that.equals("bar")`), only running them after the existing ones pass.
// So the first base-promise is `assertion._obj`, but after that we use the assertions themselves, i.e.
// previously derived promises, to chain off of.
return typeof assertion.then === 'function' ? assertion : assertion._obj;
}
function getReasonName(reason) {
return reason instanceof Error
? reason.toString()
: checkError.getConstructorName(reason);
}
// Grab these first, before we modify `Assertion.prototype`.
const propertyNames = Object.getOwnPropertyNames(Assertion.prototype);
const propertyDescs = {};
for (const name of propertyNames) {
propertyDescs[name] = Object.getOwnPropertyDescriptor(
Assertion.prototype,
name
);
}
property('fulfilled', function () {
const derivedPromise = getBasePromise(this).then(
(value) => {
assertIfNegated(
this,
'expected promise not to be fulfilled but it was fulfilled with #{act}',
{actual: value}
);
return value;
},
(reason) => {
assertIfNotNegated(
this,
'expected promise to be fulfilled but it was rejected with #{act}',
{actual: getReasonName(reason)}
);
return reason;
}
);
transferPromiseness(this, derivedPromise);
return this;
});
property('rejected', function () {
const derivedPromise = getBasePromise(this).then(
(value) => {
assertIfNotNegated(
this,
'expected promise to be rejected but it was fulfilled with #{act}',
{actual: value}
);
return value;
},
(reason) => {
assertIfNegated(
this,
'expected promise not to be rejected but it was rejected with #{act}',
{actual: getReasonName(reason)}
);
// Return the reason, transforming this into a fulfillment, to allow further assertions, e.g.
// `promise.should.be.rejected.and.eventually.equal("reason")`.
return reason;
}
);
transferPromiseness(this, derivedPromise);
return this;
});
method('rejectedWith', function (errorLike, errMsgMatcher, message) {
let errorLikeName = null;
const negate = utils.flag(this, 'negate') || false;
// rejectedWith with that is called without arguments is
// the same as a plain ".rejected" use.
if (
errorLike === undefined &&
errMsgMatcher === undefined &&
message === undefined
) {
/* eslint-disable no-unused-expressions */
return this.rejected;
/* eslint-enable no-unused-expressions */
}
if (message !== undefined) {
utils.flag(this, 'message', message);
}
if (errorLike instanceof RegExp || typeof errorLike === 'string') {
errMsgMatcher = errorLike;
errorLike = null;
} else if (errorLike && errorLike instanceof Error) {
errorLikeName = errorLike.toString();
} else if (typeof errorLike === 'function') {
errorLikeName = checkError.getConstructorName(errorLike);
} else {
errorLike = null;
}
const everyArgIsDefined = Boolean(errorLike && errMsgMatcher);
let matcherRelation = 'including';
if (errMsgMatcher instanceof RegExp) {
matcherRelation = 'matching';
}
const derivedPromise = getBasePromise(this).then(
(value) => {
let assertionMessage = null;
let expected = null;
if (errorLike) {
assertionMessage =
'expected promise to be rejected with #{exp} but it was fulfilled with #{act}';
expected = errorLikeName;
} else if (errMsgMatcher) {
assertionMessage =
`expected promise to be rejected with an error ${matcherRelation} #{exp} but ` +
`it was fulfilled with #{act}`;
expected = errMsgMatcher;
}
assertIfNotNegated(this, assertionMessage, {expected, actual: value});
return value;
},
(reason) => {
const errorLikeCompatible =
errorLike &&
(errorLike instanceof Error
? checkError.compatibleInstance(reason, errorLike)
: checkError.compatibleConstructor(reason, errorLike));
const errMsgMatcherCompatible =
errMsgMatcher === reason ||
(errMsgMatcher &&
reason &&
checkError.compatibleMessage(reason, errMsgMatcher));
const reasonName = getReasonName(reason);
if (negate && everyArgIsDefined) {
if (errorLikeCompatible && errMsgMatcherCompatible) {
this.assert(
true,
null,
'expected promise not to be rejected with #{exp} but it was rejected ' +
'with #{act}',
errorLikeName,
reasonName
);
}
} else {
if (errorLike) {
this.assert(
errorLikeCompatible,
'expected promise to be rejected with #{exp} but it was rejected with #{act}',
'expected promise not to be rejected with #{exp} but it was rejected ' +
'with #{act}',
errorLikeName,
reasonName
);
}
if (errMsgMatcher) {
this.assert(
errMsgMatcherCompatible,
`expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` +
`#{act}`,
`expected promise not to be rejected with an error ${matcherRelation} #{exp}`,
errMsgMatcher,
checkError.getMessage(reason)
);
}
}
return reason;
}
);
transferPromiseness(this, derivedPromise);
return this;
});
property('eventually', function () {
utils.flag(this, 'eventually', true);
return this;
});
method('notify', function (done) {
doNotify(getBasePromise(this), done);
return this;
});
method('become', function (value, message) {
return this.eventually.deep.equal(value, message);
});
// ### `eventually`
// We need to be careful not to trigger any getters, thus `Object.getOwnPropertyDescriptor` usage.
const methodNames = propertyNames.filter((name) => {
return name !== 'assert' && typeof propertyDescs[name].value === 'function';
});
methodNames.forEach((methodName) => {
Assertion.overwriteMethod(
methodName,
(originalMethod) =>
function () {
return doAsserterAsyncAndAddThen(originalMethod, this, arguments);
}
);
});
const getterNames = propertyNames.filter((name) => {
return name !== '_obj' && typeof propertyDescs[name].get === 'function';
});
getterNames.forEach((getterName) => {
// Chainable methods are things like `an`, which can work both for `.should.be.an.instanceOf` and as
// `should.be.an("object")`. We need to handle those specially.
const isChainableMethod = Object.prototype.hasOwnProperty.call(
Assertion.prototype.__methods,
getterName
);
if (isChainableMethod) {
Assertion.overwriteChainableMethod(
getterName,
(originalMethod) =>
function () {
return doAsserterAsyncAndAddThen(originalMethod, this, arguments);
},
(originalGetter) =>
function () {
return doAsserterAsyncAndAddThen(originalGetter, this);
}
);
} else {
Assertion.overwriteProperty(
getterName,
(originalGetter) =>
function () {
return proxifyIfSupported(
doAsserterAsyncAndAddThen(originalGetter, this)
);
}
);
}
});
function doAsserterAsyncAndAddThen(asserter, assertion, args) {
// Since we're intercepting all methods/properties, we need to just pass through if they don't want
// `eventually`, or if we've already fulfilled the promise (see below).
if (!utils.flag(assertion, 'eventually')) {
asserter.apply(assertion, args);
return assertion;
}
const derivedPromise = getBasePromise(assertion)
.then((value) => {
// Set up the environment for the asserter to actually run: `_obj` should be the fulfillment value, and
// now that we have the value, we're no longer in "eventually" mode, so we won't run any of this code,
// just the base Chai code that we get to via the short-circuit above.
assertion._obj = value;
utils.flag(assertion, 'eventually', false);
return args ? transformAsserterArgs(args) : args;
})
.then((newArgs) => {
asserter.apply(assertion, newArgs);
// Because asserters, for example `property`, can change the value of `_obj` (i.e. change the "object"
// flag), we need to communicate this value change to subsequent chained asserters. Since we build a
// promise chain paralleling the asserter chain, we can use it to communicate such changes.
return assertion._obj;
});
transferPromiseness(assertion, derivedPromise);
return assertion;
}
// ### Now use the `Assertion` framework to build an `assert` interface.
const originalAssertMethods = Object.getOwnPropertyNames(assert).filter(
(propName) => {
return typeof assert[propName] === 'function';
}
);
assert.isFulfilled = (promise, message) =>
new Assertion(promise, message).to.be.fulfilled;
assert.isRejected = (promise, errorLike, errMsgMatcher, message) => {
const assertion = new Assertion(promise, message);
return assertion.to.be.rejectedWith(errorLike, errMsgMatcher, message);
};
assert.becomes = (promise, value, message) =>
assert.eventually.deepEqual(promise, value, message);
assert.doesNotBecome = (promise, value, message) =>
assert.eventually.notDeepEqual(promise, value, message);
assert.eventually = {};
originalAssertMethods.forEach((assertMethodName) => {
assert.eventually[assertMethodName] = function (promise) {
const otherArgs = Array.prototype.slice.call(arguments, 1);
let customRejectionHandler;
const message = arguments[assert[assertMethodName].length - 1];
if (typeof message === 'string') {
customRejectionHandler = (reason) => {
throw new chai.AssertionError(
`${message}\n\nOriginal reason: ${utils.inspect(reason)}`
);
};
}
const returnedPromise = promise.then(
(fulfillmentValue) =>
assert[assertMethodName].apply(
assert,
[fulfillmentValue].concat(otherArgs)
),
customRejectionHandler
);
returnedPromise.notify = (done) => {
doNotify(returnedPromise, done);
};
return returnedPromise;
};
});
}
function defaultTransferPromiseness(assertion, promise) {
assertion.then = promise.then.bind(promise);
}
function defaultTransformAsserterArgs(values) {
return values;
}
let customTransferPromiseness;
let customTransformAsserterArgs;
export function setTransferPromiseness(fn) {
customTransferPromiseness = fn || defaultTransferPromiseness;
}
export function setTransformAsserterArgs(fn) {
customTransformAsserterArgs = fn || defaultTransformAsserterArgs;
}
export function transferPromiseness(assertion, promise) {
if (customTransferPromiseness) {
customTransferPromiseness(assertion, promise);
} else {
defaultTransferPromiseness(assertion, promise);
}
}
export function transformAsserterArgs(values) {
if (customTransformAsserterArgs) {
return customTransformAsserterArgs(values);
}
return defaultTransformAsserterArgs(values);
}