UNPKG

10.7 kBJavaScriptView Raw
1'use strict';
2
3function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
5var deepEqual = _interopDefault(require('deep-equal'));
6
7const getAssertionLocation = () => {
8 const err = new Error();
9 const stack = (err.stack || '').split('\n');
10 return (stack[3] || '').trim().replace(/^at/i, '');
11};
12
13const assertMethodHook = fn => function (...args) {
14 const assertResult = fn(...args);
15
16 if (assertResult.pass === false) {
17 assertResult.at = getAssertionLocation();
18 }
19
20 this.collect(assertResult);
21 return assertResult;
22};
23
24const Assertion = {
25 ok: assertMethodHook((val, description = 'should be truthy') => ({
26 pass: Boolean(val),
27 actual: val,
28 expected: true,
29 description,
30 operator: 'ok'
31 })),
32 deepEqual: assertMethodHook((actual, expected, description = 'should be equivalent') => ({
33 pass: deepEqual(actual, expected),
34 actual,
35 expected,
36 description,
37 operator: 'deepEqual'
38 })),
39 equal: assertMethodHook((actual, expected, description = 'should be equal') => ({
40 pass: actual === expected,
41 actual,
42 expected,
43 description,
44 operator: 'equal'
45 })),
46 notOk: assertMethodHook((val, description = 'should not be truthy') => ({
47 pass: !val,
48 expected: false,
49 actual: val,
50 description,
51 operator: 'notOk'
52 })),
53 notDeepEqual: assertMethodHook((actual, expected, description = 'should not be equivalent') => ({
54 pass: !deepEqual(actual, expected),
55 actual,
56 expected,
57 description,
58 operator: 'notDeepEqual'
59 })),
60 notEqual: assertMethodHook((actual, expected, description = 'should not be equal') => ({
61 pass: actual !== expected,
62 actual,
63 expected,
64 description,
65 operator: 'notEqual'
66 })),
67 throws: assertMethodHook((func, expected, description) => {
68 let caught;
69 let pass;
70 let actual;
71 if (typeof expected === 'string') {
72 [expected, description] = [description, expected];
73 }
74 try {
75 func();
76 } catch (err) {
77 caught = {error: err};
78 }
79 pass = caught !== undefined;
80 actual = caught && caught.error;
81 if (expected instanceof RegExp) {
82 pass = expected.test(actual) || expected.test(actual && actual.message);
83 expected = String(expected);
84 } else if (typeof expected === 'function' && caught) {
85 pass = actual instanceof expected;
86 actual = actual.constructor;
87 }
88 return {
89 pass,
90 expected,
91 actual,
92 operator: 'throws',
93 description: description || 'should throw'
94 };
95 }),
96 doesNotThrow: assertMethodHook((func, expected, description) => {
97 let caught;
98 if (typeof expected === 'string') {
99 [expected, description] = [description, expected];
100 }
101 try {
102 func();
103 } catch (err) {
104 caught = {error: err};
105 }
106 return {
107 pass: caught === undefined,
108 expected: 'no thrown error',
109 actual: caught && caught.error,
110 operator: 'doesNotThrow',
111 description: description || 'should not throw'
112 };
113 }),
114 fail: assertMethodHook((description = 'fail called') => ({
115 pass: false,
116 actual: 'fail called',
117 expected: 'fail not called',
118 description,
119 operator: 'fail'
120 }))
121};
122
123var assert = (collect, test) => Object.assign(
124 Object.create(Assertion, {collect: {value: collect}}), {
125 async test(description, spec) {
126 // Note: we return the task so the caller can control whether he wants to wait for the sub test to complete or not
127 return test(description, spec).task;
128 }
129 });
130
131const tester = (collect, {offset = 0} = {}) => (description, spec) => {
132 const buffer = [{type: 'title', data: description, offset}];
133 const result = {count: 0, pass: true, description, spec};
134 let done = false;
135
136 const createAssertion = item => {
137 result.pass = result.pass && item.pass;
138 return {type: 'assert', data: item, offset};
139 };
140
141 const collector = item => {
142 result.count++;
143 item.id = result.count;
144 if (item[Symbol.asyncIterator] === undefined) {
145 // Assertion
146 buffer.push(createAssertion(item));
147 } else {
148 // Sub test
149 buffer.push(item);
150 }
151 };
152
153 const handleDelegate = async delegate => {
154 const {value, done} = await delegate.next();
155
156 // Delegate is exhausted: create a summary test point in the stream and throw the delegate
157 if (done === true) {
158 const {executionTime, pass, description} = value;
159 const subTestAssertion = Object.assign(createAssertion({
160 pass,
161 description,
162 id: delegate.id,
163 executionTime
164 }), {type: 'testAssert'});
165 buffer.shift();
166 buffer.unshift(subTestAssertion);
167 return instance.next();
168 }
169 return {value, done};
170 };
171
172 const subTest = tester(collector, {offset: offset + 1});
173
174 const start = Date.now();
175 // Execute the test collecting assertions
176 const assertFn = assert(collector, subTest);
177 const task = new Promise(resolve => resolve(spec(assertFn)))
178 .then(() => {
179 // Always report a plan and summary: the calling test will know how to deal with it
180 result.executionTime = Date.now() - start;
181 buffer.push({type: 'plan', data: {start: 1, end: result.count}, offset});
182 buffer.push({type: 'time', data: result.executionTime, offset});
183 done = true;
184 return result;
185 })
186 .catch(err => {
187 // We report a failing test before bail out ... while unhandled promise rejection is still allowed by nodejs...
188 buffer.push({type: 'assert', data: {pass: false, description}});
189 buffer.push({type: 'comment', data: 'Unhandled exception'});
190 buffer.push({type: 'bailout', data: err, offset});
191 done = true;
192 });
193
194 const instance = {
195 test: subTest,
196 task,
197 [Symbol.asyncIterator]() {
198 return this;
199 },
200 async next() {
201 if (buffer.length === 0) {
202 if (done === true) {
203 return {done: true, value: result};
204 }
205 // Flush
206 await task;
207 return this.next();
208 }
209
210 const next = buffer[0];
211
212 // Delegate if sub test
213 if (next[Symbol.asyncIterator] !== undefined) {
214 return handleDelegate(next);
215 }
216
217 return {value: buffer.shift(), done: false};
218 }
219 };
220
221 // Collection by the calling test
222 collect(instance);
223
224 return instance;
225};
226
227const print = (message, offset = 0) => {
228 console.log(message.padStart(message.length + (offset * 4))); // 4 white space used as indent (see tap-parser)
229};
230
231const toYaml = print => (obj, offset = 0) => {
232 for (const [prop, value] of Object.entries(obj)) {
233 print(`${prop}: ${JSON.stringify(value)}`, offset + 0.5);
234 }
235};
236
237const tap = print => {
238 const yaml = toYaml(print);
239 return {
240 version(version = 13) {
241 print(`TAP version ${version}`);
242 },
243 title(value, offset = 0) {
244 const message = offset > 0 ? `Subtest: ${value}` : value;
245 this.comment(message, offset);
246 },
247 assert(value, offset = 0) {
248 const {pass, description, id, executionTime, expected = '', actual = '', at = '', operator = ''} = value;
249 const label = pass === true ? 'ok' : 'not ok';
250 print(`${label} ${id} - ${description}${executionTime ? ` # time=${executionTime}ms` : ''}`, offset);
251 if (pass === false && value.operator) {
252 print('---', offset + 0.5);
253 yaml({expected, actual, at, operator}, offset);
254 print('...', offset + 0.5);
255 }
256 },
257 plan(value, offset = 0) {
258 print(`1..${value.end}`, offset);
259 },
260 time(value, offset = 0) {
261 this.comment(`time=${value}ms`, offset);
262 },
263 comment(value, offset = 0) {
264 print(`# ${value}`, offset);
265 },
266 bailout(value = 'Unhandled exception') {
267 print(`Bail out! ${value}`);
268 },
269 testAssert(value, offset = 0) {
270 return this.assert(value, offset);
271 }
272 };
273};
274
275var tap$1 = (printFn = print) => {
276 const reporter = tap(printFn);
277 return (toPrint = {}) => {
278 const {data, type, offset = 0} = toPrint;
279 if (typeof reporter[type] === 'function') {
280 reporter[type](data, offset);
281 }
282 // Else ignore (unknown message type)
283 };
284};
285
286// Some combinators for asynchronous iterators: this will be way more easier when
287// Async generator are widely supported
288
289const asyncIterator = behavior => Object.assign({
290 [Symbol.asyncIterator]() {
291 return this;
292 }
293}, behavior);
294
295const filter = predicate => iterator => asyncIterator({
296 async next() {
297 const {done, value} = await iterator.next();
298
299 if (done === true) {
300 return {done};
301 }
302
303 if (!predicate(value)) {
304 return this.next();
305 }
306
307 return {done, value};
308 }
309});
310
311const map = mapFn => iterator => asyncIterator({
312 [Symbol.asyncIterator]() {
313 return this;
314 },
315 async next() {
316 const {done, value} = await iterator.next();
317 if (done === true) {
318 return {done};
319 }
320 return {done, value: mapFn(value)};
321 }
322});
323
324const stream = asyncIterator => Object.assign(asyncIterator, {
325 map(fn) {
326 return stream(map(fn)(asyncIterator));
327 },
328 filter(fn) {
329 return stream(filter(fn)(asyncIterator));
330 }
331});
332
333const combine = (...iterators) => {
334 const [...pending] = iterators;
335 let current = pending.shift();
336
337 return asyncIterator({
338 async next() {
339 if (current === undefined) {
340 return {done: true};
341 }
342
343 const {done, value} = await current.next();
344
345 if (done === true) {
346 current = pending.shift();
347 return this.next();
348 }
349
350 return {done, value};
351 }
352 });
353};
354
355let flatten = true;
356const tests = [];
357const test = tester(t => tests.push(t));
358
359// Provide a root context for BSD style test suite
360const subTest = (test('Root', () => {})).test;
361test.test = (description, spec) => {
362 flatten = false; // Turn reporter into BSD style
363 return subTest(description, spec);
364};
365
366const start = async ({reporter = tap$1()} = {}) => {
367 let count = 0;
368 let failure = 0;
369 reporter({type: 'version', data: 13});
370
371 // Remove the irrelevant root title
372 await tests[0].next();
373
374 let outputStream = stream(combine(...tests));
375 outputStream = flatten ? outputStream
376 .filter(({type}) => type !== 'testAssert')
377 .map(item => Object.assign(item, {offset: 0})) :
378 outputStream;
379
380 const filterOutAtRootLevel = ['plan', 'time'];
381 outputStream = outputStream
382 .filter(item => item.offset > 0 || !filterOutAtRootLevel.includes(item.type))
383 .map(item => {
384 if (item.offset > 0 || (item.type !== 'assert' && item.type !== 'testAssert')) {
385 return item;
386 }
387
388 count++;
389 item.data.id = count;
390 failure += item.data.pass ? 0 : 1;
391 return item;
392 });
393
394 // One day with for await loops ... :) !
395 while (true) {
396 const {done, value} = await outputStream.next();
397
398 if (done === true) {
399 break;
400 }
401
402 reporter(value);
403
404 if (value.type === 'bailout') {
405 throw value.data; // Rethrow but with Nodejs we keep getting the deprecation warning (unhandled promise) and the process exists with 0 exit code...
406 }
407 }
408
409 reporter({type: 'plan', data: {start: 1, end: count}});
410 reporter({type: 'comment', data: failure > 0 ? `failed ${failure} of ${count} tests` : 'ok'});
411};
412
413// Auto bootstrap following async env vs sync env (browser vs node)
414if (typeof window === 'undefined') {
415 setTimeout(start, 0);
416} else {
417 window.addEventListener('load', start);
418}
419
420module.exports = test;