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