-
Notifications
You must be signed in to change notification settings - Fork 112
/
chai-as-promised.js
391 lines (329 loc) · 17.2 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
(function (chaiAsPromised) {
"use strict";
// Module systems magic dance.
if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
// NodeJS
module.exports = chaiAsPromised;
} else if (typeof define === "function" && define.amd) {
// AMD
define(function () {
return chaiAsPromised;
});
} else {
// Other environment (usually <script> tag): plug in to global chai instance directly.
chai.use(chaiAsPromised);
}
}(function chaiAsPromised(chai, utils) {
"use strict";
var Assertion = chai.Assertion;
var assert = chai.assert;
function assertIsAboutPromise(assertion) {
if (typeof assertion._obj.then !== "function") {
throw new TypeError(utils.inspect(assertion._obj) + " is not a promise!");
}
if (typeof assertion._obj.pipe === "function") {
throw new TypeError("Chai as Promised is incompatible with jQuery's so-called “promises.” Sorry!");
}
}
function property(name, asserter) {
utils.addProperty(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return asserter.apply(this, arguments);
});
}
function method(name, asserter) {
utils.addMethod(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return asserter.apply(this, arguments);
});
}
function notify(promise, callback) {
return promise.then(function () { callback(); }, callback);
}
function addNotifyMethod(extensiblePromise) {
extensiblePromise.notify = function (callback) {
return notify(extensiblePromise, callback);
};
}
var fulfilledAsserter = function () {
var transformedPromise = this._obj.then(
function (value) {
if (utils.flag(this, "negate")) {
// If we're negated, `this.assert`'s behavior is actually flipped, so `this.assert(true, ...)` will
// throw an error, as desired.
this.assert(true, null, "expected promise to be rejected but it was fulfilled with " +
utils.inspect(value));
}
return value;
}.bind(this),
function (reason) {
// If we're in a negated state (i.e. `.not.fulfilled`) then this assertion will get flipped and thus
// pass, as desired.
this.assert(false, "expected promise to be fulfilled but it was rejected with " +
utils.inspect(reason));
}.bind(this)
);
return makeAssertionPromise(transformedPromise, this);
};
var rejectedAsserter = function () {
// THIS SHIT IS COMPLICATED. Best illustrated by exhaustive example.
////////////////////////////////////////////////////////////////////
// `fulfilledPromise.should.be.rejected`:
// `onOriginalFulfilled` → `this.assert(false, …)` throws → rejects
// `fulfilledPromise.should.not.be.rejected`:
// `onOriginalFulfilled` → `this.assert(false, …)` does nothing → fulfills
// `rejectedPromise.should.be.rejected`:
// `onOriginalRejected` does nothing relevant → fulfills
// `rejectedPromise.should.not.be.rejected`:
// `onOriginalRejected` → `this.assert(true, …)` throws → rejects
// `rejectedPromise.should.be.rejected.with(xxx)`:
// `onOriginalRejected` saves `rejectionReason` → fulfills →
// `with(xxx)` called → `onTransformedFulfilled` → assert about xxx → fulfills/rejects based on asserts
// `rejectedPromise.should.not.be.rejected.with(xxx)`:
// `onOriginalRejected` saves `rejectionReason`, `this.assert(true, …)` throws → rejects →
// `with(xxx)` called → `onTransformedRejected` → assert about xxx → fulfills/rejects based on asserts
// `fulfilledPromise.should.be.rejected.with(xxx)`:
// `onOriginalFulfilled` → `this.assert(false, …)` throws → rejects →
// `with(xxx)` called → `onTransformedRejected` → `this.assert(false, …)` throws → rejected
// `fulfilledPromise.should.not.be.rejected.with(xxx)`:
// `onOriginalFulfilled` → `this.assert(false, …)` does nothing → fulfills →
// `with(xxx)` called → `onTransformedFulfilled` → fulfills
var rejectionReason = null;
var onOriginalFulfilled = function (value) {
this.assert(false, "expected promise to be rejected but it was fulfilled with " + utils.inspect(value));
}.bind(this);
var onOriginalRejected = function (reason) {
// Store the reason so that `with` can look at it later. Be sure to do this before asserting, since
// throwing an error from the assert would cancel the process.
rejectionReason = reason;
if (utils.flag(this, "negate")) {
this.assert(true, null, "expected promise to be fulfilled but it was rejected with " +
utils.inspect(reason));
}
// If we didn't throw from the assert, transform rejections into fulfillments, by not re-throwing the
// reason.
}.bind(this);
var withMethod = function (Constructor, message) {
var desiredReason = null;
if (Constructor instanceof RegExp || typeof Constructor === "string") {
message = Constructor;
Constructor = null;
} else if (Constructor && Constructor instanceof Error) {
desiredReason = Constructor;
Constructor = null;
message = null;
}
var messageVerb = null;
var messageIsGood = null;
if (message instanceof RegExp) {
messageVerb = "matching";
messageIsGood = function () {
return message.test(rejectionReason.message);
};
} else {
messageVerb = "including";
messageIsGood = function () {
return rejectionReason.message.indexOf(message) !== -1;
};
}
function constructorIsGood() {
return rejectionReason instanceof Constructor;
}
function matchesDesiredReason() {
return rejectionReason === desiredReason;
}
var onTransformedFulfilled = function () {
if (!utils.flag(this, "negate")) {
if (desiredReason) {
this.assert(matchesDesiredReason(),
null,
"expected promise to be rejected with " + utils.inspect(desiredReason) + " but " +
"it was rejected with " + utils.inspect(rejectionReason));
}
if (Constructor) {
this.assert(constructorIsGood(),
"expected promise to be rejected with " + Constructor.prototype.name + " but it " +
"was rejected with " + utils.inspect(rejectionReason));
}
if (message) {
this.assert(messageIsGood(),
"expected promise to be rejected with an error " + messageVerb + " " + message +
" but got " + utils.inspect(rejectionReason.message));
}
}
}.bind(this);
var onTransformedRejected = function () {
if (utils.flag(this, "negate")) {
if (desiredReason) {
this.assert(matchesDesiredReason(),
null,
"expected promise to not be rejected with " + utils.inspect(desiredReason));
}
if (Constructor) {
this.assert(constructorIsGood(),
null,
"expected promise to not be rejected with " + Constructor.prototype.name);
}
if (message) {
this.assert(messageIsGood(),
null,
"expected promise to be not be rejected with an error " + messageVerb + " " +
message);
}
} else {
if (desiredReason) {
this.assert(false,
"expected promise to be rejected with " + utils.inspect(desiredReason) +
" but it was fulfilled");
}
if (Constructor) {
this.assert(false, "expected promise to be rejected with " + Constructor.prototype.name +
" but it was fulfilled");
}
if (message) {
this.assert(false, "expected promise to be rejected with an error " + messageVerb + " " +
message + " but it was fulfilled");
}
}
}.bind(this);
return makeAssertionPromise(transformedPromise.then(onTransformedFulfilled, onTransformedRejected), this);
}.bind(this);
var transformedPromise = makeAssertionPromise(this._obj.then(onOriginalFulfilled, onOriginalRejected), this);
Object.defineProperty(transformedPromise, "with", { enumerable: true, configurable: true, value: withMethod });
return transformedPromise;
};
function isChaiAsPromisedAsserter(asserterName) {
return ["fulfilled", "rejected", "broken", "eventually", "become"].indexOf(asserterName) !== -1;
}
function makeAssertionPromiseToDoAsserter(currentAssertionPromise, previousAssertionPromise, doAsserter) {
var promiseToDoAsserter = currentAssertionPromise.then(function (fulfillmentValue) {
// The previous assertion promise might have picked up some flags while waiting for fulfillment.
utils.transferFlags(previousAssertionPromise, currentAssertionPromise);
// Replace the object flag with the fulfillment value, so that doAsserter can operate properly.
utils.flag(currentAssertionPromise, "object", fulfillmentValue);
// Perform the actual asserter action and return the result of it.
return doAsserter();
});
return makeAssertionPromise(promiseToDoAsserter, currentAssertionPromise);
}
function makeAssertionPromise(promise, baseAssertion) {
// An assertion-promise is an (extensible!) promise with the following additions:
var assertionPromise = Object.create(promise);
// 1. A `notify` method.
addNotifyMethod(assertionPromise);
// 2. An `assert` method that acts exactly as it would on an assertion. This is called by promisified
// asserters after the promise fulfills.
assertionPromise.assert = function () {
return Assertion.prototype.assert.apply(assertionPromise, arguments);
};
// 3. Chai asserters, which act upon the promise's fulfillment value.
var asserterNames = Object.getOwnPropertyNames(Assertion.prototype);
asserterNames.forEach(function (asserterName) {
// We already added `notify` and `assert`; don't mess with those.
if (asserterName === "notify" || asserterName === "assert") {
return;
}
// Only add asserters for other libraries; poison-pill Chai as Promised ones.
if (isChaiAsPromisedAsserter(asserterName)) {
utils.addProperty(assertionPromise, asserterName, function () {
throw new Error("Cannot use Chai as Promised asserters more than once in an assertion.");
});
return;
}
// The asserter will need to be added differently depending on its type. In all cases we use
// `makeAssertionPromiseToDoAsserter`, which, given this current `assertionPromise` we are going to
// return, plus the `baseAssertion` we are basing it off of, will return a new assertion-promise that
// builds off of `assertionPromise` and `baseAssertion` to perform the actual asserter action upon
// fulfillment.
var propertyDescriptor = Object.getOwnPropertyDescriptor(Assertion.prototype, asserterName);
if (typeof propertyDescriptor.value === "function") {
// Case 1: simple method asserters
utils.addMethod(assertionPromise, asserterName, function () {
var args = arguments;
return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
return propertyDescriptor.value.apply(assertionPromise, args);
});
});
} else if (typeof propertyDescriptor.get === "function") {
// Case 2: property asserters. These break down into two subcases: chainable methods, and pure
// properties. An example of the former is `a`/`an`: `.should.be.an.instanceOf` vs.
// `should.be.an("object")`.
var isChainableMethod = false;
try {
isChainableMethod = typeof propertyDescriptor.get.call({}) === "function";
} catch (e) { }
if (isChainableMethod) {
// Case 2A: chainable methods. Recreate the chainable method, but operating on the augmented
// promise. We need to copy both the assertion behavior and the chaining behavior, since the
// chaining behavior might for example set flags on the object.
utils.addChainableMethod(
assertionPromise,
asserterName,
function () {
var args = arguments;
return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
return propertyDescriptor.get().apply(assertionPromise, args);
});
},
function () {
return propertyDescriptor.get.call(assertionPromise);
}
);
} else {
// Case 2B: pure property case
utils.addProperty(assertionPromise, asserterName, function () {
return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
return propertyDescriptor.get.call(assertionPromise);
});
});
}
}
});
return assertionPromise;
}
property("fulfilled", fulfilledAsserter);
property("rejected", rejectedAsserter);
property("broken", rejectedAsserter);
property("eventually", function () {
return makeAssertionPromise(this._obj, this);
});
method("become", function (value) {
return this.eventually.eql(value);
});
method("notify", function (callback) {
return notify(this._obj, callback);
});
// Now use the Assertion framework to build an `assert` interface.
var originalAssertMethods = Object.getOwnPropertyNames(assert).filter(function (propName) {
return typeof assert[propName] === "function";
});
assert.isFulfilled = function (promise, message) {
return (new Assertion(promise, message)).to.be.fulfilled;
};
assert.isRejected = assert.isBroken = function (promise, toTestAgainst, message) {
if (typeof toTestAgainst === "string") {
message = toTestAgainst;
toTestAgainst = null;
}
var shouldBeRejectedPromise = (new Assertion(promise, message)).to.be.rejected;
return toTestAgainst ? shouldBeRejectedPromise.with(toTestAgainst) : shouldBeRejectedPromise;
};
assert.eventually = {};
originalAssertMethods.forEach(function (assertMethodName) {
assert.eventually[assertMethodName] = function (promise) {
var otherArgs = Array.prototype.slice.call(arguments, 1);
var promiseToAssert = promise.then(function (fulfillmentValue) {
return assert[assertMethodName].apply(assert, [fulfillmentValue].concat(otherArgs));
});
var augmentedPromiseToAssert = Object.create(promiseToAssert);
addNotifyMethod(augmentedPromiseToAssert);
return augmentedPromiseToAssert;
};
});
assert.becomes = function (promise, value) {
return assert.eventually.deepEqual(promise, value);
};
assert.doesNotBecome = function (promise, value) {
return assert.eventually.notDeepEqual(promise, value);
};
}));