1 | 'use strict';
|
2 | var isGeneratorFn = require('is-generator-fn');
|
3 | var maxTimeout = require('max-timeout');
|
4 | var Promise = require('bluebird');
|
5 | var fnName = require('fn-name');
|
6 | var co = require('co-with-promise');
|
7 | var observableToPromise = require('observable-to-promise');
|
8 | var isPromise = require('is-promise');
|
9 | var isObservable = require('is-observable');
|
10 | var inspect = require('util').inspect;
|
11 | var assert = require('./assert');
|
12 | var enhanceAssert = require('./enhance-assert');
|
13 | var globals = require('./globals');
|
14 |
|
15 | function 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 |
|
39 |
|
40 | this.metadata = {};
|
41 |
|
42 |
|
43 |
|
44 | this._timeStart = null;
|
45 |
|
46 |
|
47 | if (this.title === 'callee$0$0') {
|
48 | this.title = '[anonymous]';
|
49 | }
|
50 | }
|
51 |
|
52 | module.exports = Test;
|
53 |
|
54 | Object.defineProperty(Test.prototype, 'assertCount', {
|
55 | enumerable: true,
|
56 | get: function () {
|
57 | return this.assertions.length;
|
58 | }
|
59 | });
|
60 |
|
61 | Test.prototype._assert = function (promise) {
|
62 | if (isPromise(promise)) {
|
63 | this.sync = false;
|
64 | }
|
65 |
|
66 | this.assertions.push(promise);
|
67 | };
|
68 |
|
69 | Test.prototype._setAssertError = function (err) {
|
70 | if (this.assertError !== undefined) {
|
71 | return;
|
72 | }
|
73 |
|
74 | this.assertError = err;
|
75 | };
|
76 |
|
77 | Test.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 |
|
85 |
|
86 | this.planStack = planStack;
|
87 | };
|
88 |
|
89 | Test.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 |
|
109 | Test.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) {
|
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 |
|
128 | Test.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 |
|
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 |
|
183 | Test.prototype._result = function () {
|
184 | var passed = this.assertError === undefined;
|
185 | return {passed: passed, result: this, reason: this.assertError};
|
186 | };
|
187 |
|
188 | Object.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 |
|
198 | Test.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 |
|
221 | Test.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 |
|
234 | Test.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 |
|
257 | self.duration = globals.now() - self._timeStart;
|
258 |
|
259 |
|
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 |
|
270 | Test.prototype._publicApi = function () {
|
271 | return new PublicApi(this);
|
272 | };
|
273 |
|
274 | function PublicApi(test) {
|
275 | this._test = test;
|
276 | this.skip = new SkipApi(test);
|
277 | }
|
278 |
|
279 | function 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 |
|
307 | PublicApi.prototype = enhanceAssert({
|
308 | assert: assert,
|
309 | onSuccess: onAssertionEvent,
|
310 | onError: onAssertionEvent
|
311 | });
|
312 |
|
313 | PublicApi.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 |
|
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 |
|
338 | Object.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 |
|
356 | function skipFn() {
|
357 | return this._test._assert(null);
|
358 | }
|
359 |
|
360 | function SkipApi(test) {
|
361 | this._test = test;
|
362 | }
|
363 |
|
364 | Object.keys(assert).forEach(function (el) {
|
365 | SkipApi.prototype[el] = skipFn;
|
366 | });
|