UNPKG

8.14 kBJavaScriptView Raw
1'use strict';
2var isGeneratorFn = require('is-generator-fn');
3var maxTimeout = require('max-timeout');
4var Promise = require('bluebird');
5var fnName = require('fn-name');
6var co = require('co-with-promise');
7var observableToPromise = require('observable-to-promise');
8var isPromise = require('is-promise');
9var isObservable = require('is-observable');
10var inspect = require('util').inspect;
11var assert = require('./assert');
12var enhanceAssert = require('./enhance-assert');
13var globals = require('./globals');
14
15function Test(title, fn, contextRef, report) {
16 if (!(this instanceof Test)) {
17 throw new TypeError('Class constructor Test cannot be invoked without \'new\'');
18 }
19
20 if (typeof title === 'function') {
21 contextRef = fn;
22 fn = title;
23 title = null;
24 }
25
26 assert.is(typeof fn, 'function', 'you must provide a callback');
27
28 this.title = title || fnName(fn) || '[anonymous]';
29 this.fn = isGeneratorFn(fn) ? co.wrap(fn) : fn;
30 this.assertions = [];
31 this.planCount = null;
32 this.duration = null;
33 this.assertError = undefined;
34 this.sync = true;
35 this.contextRef = contextRef;
36 this.report = report;
37
38 // TODO(jamestalmage): make this an optional constructor arg instead of having Runner set it after the fact.
39 // metadata should just always exist, otherwise it requires a bunch of ugly checks all over the place.
40 this.metadata = {};
41
42 // store the time point before test execution
43 // to calculate the total time spent in test
44 this._timeStart = null;
45
46 // workaround for Babel giving anonymous functions a name
47 if (this.title === 'callee$0$0') {
48 this.title = '[anonymous]';
49 }
50}
51
52module.exports = Test;
53
54Object.defineProperty(Test.prototype, 'assertCount', {
55 enumerable: true,
56 get: function () {
57 return this.assertions.length;
58 }
59});
60
61Test.prototype._assert = function (promise) {
62 if (isPromise(promise)) {
63 this.sync = false;
64 }
65
66 this.assertions.push(promise);
67};
68
69Test.prototype._setAssertError = function (err) {
70 if (this.assertError !== undefined) {
71 return;
72 }
73
74 this.assertError = err;
75};
76
77Test.prototype.plan = function (count, planStack) {
78 if (typeof count !== 'number') {
79 throw new TypeError('Expected a number');
80 }
81
82 this.planCount = count;
83
84 // in case the `planCount` doesn't match `assertCount,
85 // we need the stack of this function to throw with a useful stack
86 this.planStack = planStack;
87};
88
89Test.prototype._run = function () {
90 var ret;
91
92 try {
93 ret = this.fn(this._publicApi());
94 } catch (err) {
95 if (err instanceof Error) {
96 this._setAssertError(err);
97 } else {
98 this._setAssertError(new assert.AssertionError({
99 actual: err,
100 message: 'Non-error thrown with value: ' + inspect(err, {depth: null}),
101 operator: 'catch'
102 }));
103 }
104 }
105
106 return ret;
107};
108
109Test.prototype.promise = function () {
110 var self = this;
111
112 if (!this._promise) {
113 this._promise = {};
114
115 this._promise.promise = new Promise(function (resolve, reject) { // eslint-disable-line
116 self._promise.resolve = resolve;
117 self._promise.reject = reject;
118 }).tap(function (result) {
119 if (self.report) {
120 self.report(result);
121 }
122 });
123 }
124
125 return this._promise;
126};
127
128Test.prototype.run = function () {
129 if (this.metadata.callback) {
130 this.sync = false;
131 }
132
133 var self = this;
134
135 this._timeStart = globals.now();
136
137 // wait until all assertions are complete
138 this._timeout = globals.setTimeout(function () {}, maxTimeout);
139
140 var ret = this._run();
141
142 var asyncType = 'promises';
143
144 if (isObservable(ret)) {
145 asyncType = 'observables';
146 ret = observableToPromise(ret);
147 }
148
149 if (isPromise(ret)) {
150 this.sync = false;
151
152 if (this.metadata.callback) {
153 self._setAssertError(new Error('Do not return ' + asyncType + ' from tests declared via `test.cb(...)`, if you want to return a promise simply declare the test via `test(...)`'));
154 }
155
156 ret.then(
157 function () {
158 self.exit();
159 },
160 function (err) {
161 if (!(err instanceof Error)) {
162 err = new assert.AssertionError({
163 actual: err,
164 message: 'Promise rejected with: ' + inspect(err, {depth: null}),
165 operator: 'promise'
166 });
167 }
168
169 self._setAssertError(err);
170 self.exit();
171 });
172
173 return this.promise().promise;
174 }
175
176 if (this.metadata.callback) {
177 return this.promise().promise;
178 }
179
180 return this.exit();
181};
182
183Test.prototype._result = function () {
184 var passed = this.assertError === undefined;
185 return {passed: passed, result: this, reason: this.assertError};
186};
187
188Object.defineProperty(Test.prototype, 'end', {
189 get: function () {
190 if (this.metadata.callback) {
191 return this._end.bind(this);
192 }
193
194 throw new Error('t.end is not supported in this context. To use t.end as a callback, you must use "callback mode" via `test.cb(testName, fn)` ');
195 }
196});
197
198Test.prototype._end = function (err) {
199 if (err) {
200 if (!(err instanceof Error)) {
201 err = new assert.AssertionError({
202 actual: err,
203 message: 'Callback called with an error: ' + inspect(err, {depth: null}),
204 operator: 'callback'
205 });
206 }
207 this._setAssertError(err);
208
209 this.exit();
210 return;
211 }
212
213 if (this.endCalled) {
214 throw new Error('.end() called more than once');
215 }
216
217 this.endCalled = true;
218 this.exit();
219};
220
221Test.prototype._checkPlanCount = function () {
222 if (this.assertError === undefined && this.planCount !== null && this.planCount !== this.assertions.length) {
223 this._setAssertError(new assert.AssertionError({
224 actual: this.assertions.length,
225 expected: this.planCount,
226 message: 'Assertion count does not match planned',
227 operator: 'plan'
228 }));
229
230 this.assertError.stack = this.planStack;
231 }
232};
233
234Test.prototype.exit = function () {
235 var self = this;
236
237 this._checkPlanCount();
238
239 if (this.sync) {
240 self.duration = globals.now() - self._timeStart;
241 globals.clearTimeout(self._timeout);
242 var result = this._result();
243
244 if (this.report) {
245 this.report(result);
246 }
247
248 return result;
249 }
250
251 Promise.all(this.assertions)
252 .catch(function (err) {
253 self._setAssertError(err);
254 })
255 .finally(function () {
256 // calculate total time spent in test
257 self.duration = globals.now() - self._timeStart;
258
259 // stop infinite timer
260 globals.clearTimeout(self._timeout);
261
262 self._checkPlanCount();
263
264 self.promise().resolve(self._result());
265 });
266
267 return self.promise().promise;
268};
269
270Test.prototype._publicApi = function () {
271 return new PublicApi(this);
272};
273
274function PublicApi(test) {
275 this._test = test;
276 this.skip = new SkipApi(test);
277}
278
279function onAssertionEvent(event) {
280 var promise = null;
281
282 if (event.assertionThrew) {
283 event.error.powerAssertContext = event.powerAssertContext;
284 event.error.originalMessage = event.originalMessage;
285 this._test._setAssertError(event.error);
286 } else {
287 var ret = event.returnValue;
288
289 if (isObservable(ret)) {
290 ret = observableToPromise(ret);
291 }
292
293 if (isPromise(ret)) {
294 promise = ret
295 .then(null, function (err) {
296 err.originalMessage = event.originalMessage;
297 throw err;
298 });
299 }
300 }
301
302 this._test._assert(promise);
303
304 return promise;
305}
306
307PublicApi.prototype = enhanceAssert({
308 assert: assert,
309 onSuccess: onAssertionEvent,
310 onError: onAssertionEvent
311});
312
313PublicApi.prototype.plan = function plan(ct) {
314 var limitBefore = Error.stackTraceLimit;
315 Error.stackTraceLimit = 1;
316 var obj = {};
317 Error.captureStackTrace(obj, plan);
318 Error.stackTraceLimit = limitBefore;
319 this._test.plan(ct, obj.stack);
320};
321
322// Getters
323[
324 'assertCount',
325 'title',
326 'end'
327]
328 .forEach(function (name) {
329 Object.defineProperty(PublicApi.prototype, name, {
330 enumerable: false,
331 get: function () {
332 return this._test[name];
333 }
334 });
335 });
336
337// Get / Set
338Object.defineProperty(PublicApi.prototype, 'context', {
339 enumerable: true,
340 get: function () {
341 var contextRef = this._test.contextRef;
342 return contextRef && contextRef.context;
343 },
344 set: function (context) {
345 var contextRef = this._test.contextRef;
346
347 if (!contextRef) {
348 this._test._setAssertError(new Error('t.context is not available in ' + this._test.metadata.type + ' tests'));
349 return;
350 }
351
352 contextRef.context = context;
353 }
354});
355
356function skipFn() {
357 return this._test._assert(null);
358}
359
360function SkipApi(test) {
361 this._test = test;
362}
363
364Object.keys(assert).forEach(function (el) {
365 SkipApi.prototype[el] = skipFn;
366});