UNPKG

15.9 kBJavaScriptView Raw
1const Operator = {
2 EQUAL: 'equal',
3 NOT_EQUAL: 'notEqual',
4 IS: 'is',
5 OK: 'ok',
6 NOT_OK: 'notOk',
7 IS_NOT: 'isNot',
8 FAIL: 'fail',
9 THROWS: 'throws',
10};
11
12const specFnRegexp = /zora_spec_fn/;
13const zoraInternal = /zora\/dist/;
14const filterStackLine = (l) =>
15 (l && !zoraInternal.test(l) && !l.startsWith('Error')) ||
16 specFnRegexp.test(l);
17
18const getAssertionLocation = () => {
19 const err = new Error();
20 const stack = (err.stack || '')
21 .split('\n')
22 .map((l) => l.trim())
23 .filter(filterStackLine);
24 const userLandIndex = stack.findIndex((l) => specFnRegexp.test(l));
25 const stackline =
26 userLandIndex >= 1 ? stack[userLandIndex - 1] : stack[0] || 'N/A';
27 return stackline.replace(/^at|^@/, '');
28};
29
30const decorateWithLocation = (result) => {
31 if (result.pass === false) {
32 return {
33 ...result,
34 at: getAssertionLocation(),
35 };
36 }
37 return result;
38};
39
40// do not edit .js files directly - edit src/index.jst
41
42
43
44var fastDeepEqual = function equal(a, b) {
45 if (a === b) return true;
46
47 if (a && b && typeof a == 'object' && typeof b == 'object') {
48 if (a.constructor !== b.constructor) return false;
49
50 var length, i, keys;
51 if (Array.isArray(a)) {
52 length = a.length;
53 if (length != b.length) return false;
54 for (i = length; i-- !== 0;)
55 if (!equal(a[i], b[i])) return false;
56 return true;
57 }
58
59
60
61 if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
62 if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
63 if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
64
65 keys = Object.keys(a);
66 length = keys.length;
67 if (length !== Object.keys(b).length) return false;
68
69 for (i = length; i-- !== 0;)
70 if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
71
72 for (i = length; i-- !== 0;) {
73 var key = keys[i];
74
75 if (!equal(a[key], b[key])) return false;
76 }
77
78 return true;
79 }
80
81 // true if both NaN, false otherwise
82 return a!==a && b!==b;
83};
84
85var eq = fastDeepEqual;
86
87const equal = (
88 actual,
89 expected,
90 description = 'should be equivalent'
91) => ({
92 pass: eq(actual, expected),
93 actual,
94 expected,
95 description,
96 operator: Operator.EQUAL,
97});
98
99const notEqual = (
100 actual,
101 expected,
102 description = 'should not be equivalent'
103) => ({
104 pass: !eq(actual, expected),
105 actual,
106 expected,
107 description,
108 operator: Operator.NOT_EQUAL,
109});
110
111const is = (actual, expected, description = 'should be the same') => ({
112 pass: Object.is(actual, expected),
113 actual,
114 expected,
115 description,
116 operator: Operator.IS,
117});
118
119const isNot = (
120 actual,
121 expected,
122 description = 'should not be the same'
123) => ({
124 pass: !Object.is(actual, expected),
125 actual,
126 expected,
127 description,
128 operator: Operator.IS_NOT,
129});
130
131const ok = (actual, description = 'should be truthy') => ({
132 pass: Boolean(actual),
133 actual,
134 expected: 'truthy value',
135 description,
136 operator: Operator.OK,
137});
138
139const notOk = (actual, description = 'should be falsy') => ({
140 pass: !Boolean(actual),
141 actual,
142 expected: 'falsy value',
143 description,
144 operator: Operator.NOT_OK,
145});
146
147const fail = (description = 'fail called') => ({
148 pass: false,
149 actual: 'fail called',
150 expected: 'fail not called',
151 description,
152 operator: Operator.FAIL,
153});
154
155const throws = (func, expected, description = 'should throw') => {
156 let caught;
157 let pass;
158 let actual;
159 if (typeof expected === 'string') {
160 [expected, description] = [void 0, expected];
161 }
162 try {
163 func();
164 } catch (err) {
165 caught = { error: err };
166 }
167 pass = caught !== undefined;
168 actual = caught?.error;
169
170 if (expected instanceof RegExp) {
171 pass = expected.test(actual) || expected.test(actual && actual.message);
172 actual = actual?.message ?? actual;
173 expected = String(expected);
174 } else if (typeof expected === 'function' && caught) {
175 pass = actual instanceof expected;
176 actual = actual.constructor;
177 } else {
178 actual = pass ? 'error thrown' : 'no error thrown';
179 }
180
181 return {
182 pass,
183 actual,
184 expected: expected ?? 'any error thrown',
185 description: description,
186 operator: Operator.THROWS,
187 };
188};
189
190const Assert$1 = {
191 equal,
192 equals: equal,
193 eq: equal,
194 deepEqual: equal,
195 same: equal,
196 notEqual,
197 notEquals: notEqual,
198 notEq: notEqual,
199 notDeepEqual: notEqual,
200 notSame: notEqual,
201 is,
202 isNot,
203 ok,
204 truthy: ok,
205 notOk,
206 falsy: notOk,
207 fail,
208 throws,
209};
210
211const noop$1 = () => {};
212
213const hook = (onResult) => (assert) =>
214 Object.fromEntries(
215 Object.keys(Assert$1).map((methodName) => [
216 methodName,
217 (...args) => onResult(assert[methodName](...args)),
218 ])
219 );
220
221var assertFactory = (
222 { onResult = noop$1 } = {
223 onResult: noop$1,
224 }
225) => {
226 const hookOnAssert = hook((item) => {
227 const result = decorateWithLocation(item);
228 onResult(result);
229 return result;
230 });
231
232 return hookOnAssert(Object.create(Assert$1));
233};
234
235const Assert = Assert$1;
236
237const MESSAGE_TYPE = {
238 TEST_START: 'TEST_START',
239 ASSERTION: 'ASSERTION',
240 TEST_END: 'TEST_END',
241 ERROR: 'ERROR',
242 UNKNOWN: 'UNKNOWN',
243};
244
245const newTestMessage = ({ description, skip }) => ({
246 type: MESSAGE_TYPE.TEST_START,
247 data: { description, skip },
248});
249
250const assertionMessage = (data) => ({
251 type: MESSAGE_TYPE.ASSERTION,
252 data,
253});
254
255const testEndMessage = ({ description, executionTime }) => ({
256 type: MESSAGE_TYPE.TEST_END,
257 data: {
258 description,
259 executionTime,
260 },
261});
262
263const errorMessage = ({ error }) => ({
264 type: MESSAGE_TYPE.ERROR,
265 data: {
266 error,
267 },
268});
269
270const isNode$1 = typeof process !== 'undefined';
271
272const flatDiagnostic = ({ pass, description, ...rest }) => rest;
273
274const createReplacer = () => {
275 const visited = new Set();
276 return (key, value) => {
277 if (isObject(value)) {
278 if (visited.has(value)) {
279 return '[__CIRCULAR_REF__]';
280 }
281
282 visited.add(value);
283 }
284
285 if (typeof value === 'symbol') {
286 return value.toString();
287 }
288
289 return value;
290 };
291};
292
293const isObject = (candidate) =>
294 typeof candidate === 'object' && candidate !== null;
295
296const stringify = (value) => JSON.stringify(value, createReplacer());
297
298const defaultSerializer = stringify;
299
300const defaultLogger = (value) => console.log(value);
301
302const isAssertionFailing = (message) =>
303 message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass;
304
305const isSkipped = (message) =>
306 message.type === MESSAGE_TYPE.TEST_START && message.data.skip;
307
308const eventuallySetExitCode = (message) => {
309 if (isNode$1 && isAssertionFailing(message)) {
310 process.exitCode = 1;
311 }
312};
313
314const compose = (fns) => (arg) =>
315 fns.reduceRight((arg, fn) => fn(arg), arg);
316
317const filter = (predicate) =>
318 async function* (stream) {
319 for await (const element of stream) {
320 if (predicate(element)) {
321 yield element;
322 }
323 }
324 };
325
326const idSequence = () => {
327 let id = 0;
328 return () => ++id;
329};
330
331const createCounter = () => {
332 const nextId = idSequence();
333 let success = 0;
334 let failure = 0;
335 let skip = 0;
336
337 return Object.create(
338 {
339 increment(message) {
340 const { type } = message;
341 if (isSkipped(message)) {
342 skip++;
343 } else if (type === MESSAGE_TYPE.ASSERTION) {
344 success += message.data.pass === true ? 1 : 0;
345 failure += message.data.pass === false ? 1 : 0;
346 }
347 },
348 nextId,
349 },
350 {
351 success: {
352 enumerable: true,
353 get() {
354 return success;
355 },
356 },
357 failure: {
358 enumerable: true,
359 get() {
360 return failure;
361 },
362 },
363 skip: {
364 enumerable: true,
365 get() {
366 return skip;
367 },
368 },
369 total: {
370 enumerable: true,
371 get() {
372 return skip + failure + success;
373 },
374 },
375 }
376 );
377};
378
379const createWriter = ({
380 log = defaultLogger,
381 serialize = defaultSerializer,
382 version = 13,
383} = {}) => {
384 const print = (message, padding = 0) => {
385 log(message.padStart(message.length + padding * 4)); // 4 white space used as indent
386 };
387
388 const printYAML = (obj, padding = 0) => {
389 const YAMLPadding = padding + 0.5;
390 print('---', YAMLPadding);
391 for (const [prop, value] of Object.entries(obj)) {
392 print(`${prop}: ${serialize(value)}`, YAMLPadding + 0.5);
393 }
394 print('...', YAMLPadding);
395 };
396
397 const printComment = (comment, padding = 0) => {
398 print(`# ${comment}`, padding);
399 };
400
401 const printBailOut = () => {
402 print('Bail out! Unhandled error.');
403 };
404
405 const printTestStart = (newTestMessage) => {
406 const {
407 data: { description },
408 } = newTestMessage;
409 printComment(description);
410 };
411
412 const printAssertion = (assertionMessage, { id, comment = '' }) => {
413 const { data } = assertionMessage;
414 const { pass, description } = data;
415 const label = pass === true ? 'ok' : 'not ok';
416 const directiveComment = comment ? ` # ${comment}` : '';
417 print(`${label} ${id} - ${description}` + directiveComment);
418 if (pass === false) {
419 printYAML(flatDiagnostic(data));
420 }
421 };
422
423 const printSummary = ({ success, skip, failure, total }) => {
424 print('', 0);
425 print(`1..${total}`);
426 printComment(`tests ${total}`, 0);
427 printComment(`pass ${success}`, 0);
428 printComment(`fail ${failure}`, 0);
429 printComment(`skip ${skip}`, 0);
430 };
431
432 const printHeader = () => {
433 print(`TAP version ${version}`);
434 };
435
436 return {
437 print,
438 printYAML,
439 printComment,
440 printBailOut,
441 printTestStart,
442 printAssertion,
443 printSummary,
444 printHeader,
445 };
446};
447
448const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END;
449const filterOutTestEnd = filter(isNotTestEnd);
450
451const writeMessage = ({ writer, nextId }) => {
452 const writerTable = {
453 [MESSAGE_TYPE.ASSERTION](message) {
454 return writer.printAssertion(message, { id: nextId() });
455 },
456 [MESSAGE_TYPE.TEST_START](message) {
457 if (message.data.skip) {
458 const skippedAssertionMessage = assertionMessage({
459 description: message.data.description,
460 pass: true,
461 });
462 return writer.printAssertion(skippedAssertionMessage, {
463 comment: 'SKIP',
464 id: nextId(),
465 });
466 }
467 return writer.printTestStart(message);
468 },
469 [MESSAGE_TYPE.ERROR](message) {
470 writer.printBailOut();
471 throw message.data.error;
472 },
473 };
474 return (message) => writerTable[message.type]?.(message);
475};
476
477var createTAPReporter = ({ log = defaultLogger, serialize = defaultSerializer } = {}) =>
478 async (messageStream) => {
479 const writer = createWriter({
480 log,
481 serialize,
482 });
483 const counter = createCounter();
484 const write = writeMessage({ writer, nextId: counter.nextId });
485 const stream = filterOutTestEnd(messageStream);
486
487 writer.printHeader();
488 for await (const message of stream) {
489 counter.increment(message);
490 write(message);
491 eventuallySetExitCode(message);
492 }
493 writer.printSummary(counter);
494 };
495
496var createJSONReporter = ({
497 log = defaultLogger,
498 serialize = defaultSerializer,
499} = {}) => {
500 const print = compose([log, serialize]);
501 return async (messageStream) => {
502 for await (const message of messageStream) {
503 eventuallySetExitCode(message);
504 print(message);
505 }
506 };
507};
508
509const findConfigurationValue = (name) => {
510 if (isNode) {
511 return process.env[name];
512 } else if (isDeno) {
513 return Deno.env.get(name);
514 } else if (isBrowser) {
515 return window[name];
516 }
517};
518
519const isNode = typeof process !== 'undefined';
520const isBrowser = typeof window !== 'undefined';
521const isDeno = typeof Deno !== 'undefined';
522
523const DEFAULT_TIMEOUT = 5_000;
524const defaultOptions = Object.freeze({
525 skip: false,
526 timeout: findConfigurationValue('ZORA_TIMEOUT') || DEFAULT_TIMEOUT,
527});
528const noop = () => {};
529
530const isTest = (assertionLike) =>
531 assertionLike[Symbol.asyncIterator] !== void 0;
532
533Assert.test = (description, spec, opts = defaultOptions) =>
534 test$1(description, spec, opts);
535
536Assert.skip = (description, spec, opts = defaultOptions) =>
537 test$1(description, spec, { ...opts, skip: true });
538
539Assert.only = () => {
540 throw new Error(`Can not use "only" method when not in "run only" mode`);
541};
542
543const createTimeoutResult = ({ timeout }) => ({
544 operator: 'timeout',
545 pass: false,
546 actual: `test takes longer than ${timeout}ms to complete`,
547 expected: `test takes less than ${timeout}ms to complete`,
548 description:
549 'The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info',
550});
551
552const test$1 = (description, spec, opts = defaultOptions) => {
553 const { skip = false, timeout = DEFAULT_TIMEOUT } = opts;
554 const assertions = [];
555 let executionTime;
556 let done = false;
557 let error;
558
559 const onResult = (assertion) => {
560 if (done) {
561 throw new Error(`test "${description}"
562tried to collect an assertion after it has run to its completion.
563You might have forgotten to wait for an asynchronous task to complete
564------
565${spec.toString()}`);
566 }
567
568 assertions.push(assertion);
569 };
570
571 const specFn = skip
572 ? noop
573 : function zora_spec_fn() {
574 return spec(assertFactory({ onResult }));
575 };
576
577 const testRoutine = (async function () {
578 try {
579 let timeoutId;
580 const start = Date.now();
581 const result = await Promise.race([
582 specFn(),
583 new Promise((resolve) => {
584 timeoutId = setTimeout(() => {
585 onResult(createTimeoutResult({ timeout }));
586 resolve();
587 }, timeout);
588 }),
589 ]);
590 clearTimeout(timeoutId);
591 executionTime = Date.now() - start;
592 return result;
593 } catch (e) {
594 error = e;
595 } finally {
596 done = true;
597 }
598 })();
599
600 return Object.assign(testRoutine, {
601 [Symbol.asyncIterator]: async function* () {
602 yield newTestMessage({ description, skip });
603 await testRoutine;
604 for (const assertion of assertions) {
605 if (isTest(assertion)) {
606 yield* assertion;
607 } else {
608 yield assertionMessage(assertion);
609 }
610 }
611
612 if (error) {
613 yield errorMessage({ error });
614 }
615
616 yield testEndMessage({ description, executionTime });
617 },
618 });
619};
620
621const createAssert = assertFactory;
622
623const createHarness$1 = ({ onlyMode = false } = {}) => {
624 const tests = [];
625
626 // WARNING if the "onlyMode is passed to any harness, all the harnesses will be affected.
627 // However, we do not expect multiple harnesses to be used in the same process
628 if (onlyMode) {
629 const { skip, test } = Assert;
630 Assert.test = skip;
631 Assert.only = test;
632 }
633
634 const { test, skip, only } = createAssert({
635 onResult: (test) => tests.push(test),
636 });
637
638 // for convenience
639 test.only = only;
640 test.skip = skip;
641
642 return {
643 only,
644 test,
645 skip,
646 report({ reporter }) {
647 return reporter(createMessageStream(tests));
648 },
649 };
650};
651
652async function* createMessageStream(tests) {
653 for (const test of tests) {
654 yield* test;
655 }
656}
657
658let autoStart = true;
659
660const harness = createHarness$1({
661 onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0,
662});
663
664const only = harness.only;
665
666const test = harness.test;
667
668const skip = harness.skip;
669
670const report = harness.report;
671
672const hold = () => !(autoStart = false);
673
674const createHarness = (opts) => {
675 hold();
676 return createHarness$1(opts);
677};
678
679const start = async () => {
680 if (autoStart) {
681 const reporter =
682 findConfigurationValue('ZORA_REPORTER') === 'json'
683 ? createJSONReporter()
684 : createTAPReporter();
685 await report({ reporter });
686 }
687};
688
689// on next tick start reporting
690if (!isBrowser) {
691 setTimeout(start, 0);
692} else {
693 window.addEventListener('load', start);
694}
695
696export { Assert, createHarness, createJSONReporter, createTAPReporter, hold, only, report, skip, test };