UNPKG

15 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 }
178 return {
179 pass,
180 actual,
181 expected,
182 description: description,
183 operator: Operator.THROWS,
184 };
185};
186
187const Assert$1 = {
188 equal,
189 equals: equal,
190 eq: equal,
191 deepEqual: equal,
192 same: equal,
193 notEqual,
194 notEquals: notEqual,
195 notEq: notEqual,
196 notDeepEqual: notEqual,
197 notSame: notEqual,
198 is,
199 isNot,
200 ok,
201 truthy: ok,
202 notOk,
203 falsy: notOk,
204 fail,
205 throws,
206};
207
208const noop$1 = () => {};
209
210const hook = (onResult) => (assert) =>
211 Object.fromEntries(
212 Object.keys(Assert$1).map((methodName) => [
213 methodName,
214 (...args) => onResult(assert[methodName](...args)),
215 ])
216 );
217
218var assertFactory = (
219 { onResult = noop$1 } = {
220 onResult: noop$1,
221 }
222) => {
223 const hookOnAssert = hook((item) => {
224 const result = decorateWithLocation(item);
225 onResult(result);
226 return result;
227 });
228
229 return hookOnAssert(Object.create(Assert$1));
230};
231
232const Assert = Assert$1;
233
234const MESSAGE_TYPE = {
235 TEST_START: 'TEST_START',
236 ASSERTION: 'ASSERTION',
237 TEST_END: 'TEST_END',
238 ERROR: 'ERROR',
239 UNKNOWN: 'UNKNOWN',
240};
241
242const newTestMessage = ({ description, skip }) => ({
243 type: MESSAGE_TYPE.TEST_START,
244 data: { description, skip },
245});
246
247const assertionMessage = (data) => ({
248 type: MESSAGE_TYPE.ASSERTION,
249 data,
250});
251
252const testEndMessage = ({ description, executionTime }) => ({
253 type: MESSAGE_TYPE.TEST_END,
254 data: {
255 description,
256 executionTime,
257 },
258});
259
260const errorMessage = ({ error }) => ({
261 type: MESSAGE_TYPE.ERROR,
262 data: {
263 error,
264 },
265});
266
267const isNode$1 = typeof process !== 'undefined';
268
269const flatDiagnostic = ({ pass, description, ...rest }) => rest;
270
271const createReplacer = () => {
272 const visited = new Set();
273 return (key, value) => {
274 if (isObject(value)) {
275 if (visited.has(value)) {
276 return '[__CIRCULAR_REF__]';
277 }
278
279 visited.add(value);
280 }
281
282 if (typeof value === 'symbol') {
283 return value.toString();
284 }
285
286 return value;
287 };
288};
289
290const isObject = (candidate) =>
291 typeof candidate === 'object' && candidate !== null;
292
293const stringify = (value) => JSON.stringify(value, createReplacer());
294
295const defaultSerializer = stringify;
296
297const defaultLogger = (value) => console.log(value);
298
299const isAssertionFailing = (message) =>
300 message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass;
301
302const isSkipped = (message) =>
303 message.type === MESSAGE_TYPE.TEST_START && message.data.skip;
304
305const eventuallySetExitCode = (message) => {
306 if (isNode$1 && isAssertionFailing(message)) {
307 process.exitCode = 1;
308 }
309};
310
311const compose = (fns) => (arg) =>
312 fns.reduceRight((arg, fn) => fn(arg), arg);
313
314const filter = (predicate) =>
315 async function* (stream) {
316 for await (const element of stream) {
317 if (predicate(element)) {
318 yield element;
319 }
320 }
321 };
322
323const idSequence = () => {
324 let id = 0;
325 return () => ++id;
326};
327
328const createCounter = () => {
329 const nextId = idSequence();
330 let success = 0;
331 let failure = 0;
332 let skip = 0;
333
334 return Object.create(
335 {
336 increment(message) {
337 const { type } = message;
338 if (isSkipped(message)) {
339 skip++;
340 } else if (type === MESSAGE_TYPE.ASSERTION) {
341 success += message.data.pass === true ? 1 : 0;
342 failure += message.data.pass === false ? 1 : 0;
343 }
344 },
345 nextId,
346 },
347 {
348 success: {
349 enumerable: true,
350 get() {
351 return success;
352 },
353 },
354 failure: {
355 enumerable: true,
356 get() {
357 return failure;
358 },
359 },
360 skip: {
361 enumerable: true,
362 get() {
363 return skip;
364 },
365 },
366 total: {
367 enumerable: true,
368 get() {
369 return skip + failure + success;
370 },
371 },
372 }
373 );
374};
375
376const createWriter = ({
377 log = defaultLogger,
378 serialize = defaultSerializer,
379 version = 13,
380} = {}) => {
381 const print = (message, padding = 0) => {
382 log(message.padStart(message.length + padding * 4)); // 4 white space used as indent
383 };
384
385 const printYAML = (obj, padding = 0) => {
386 const YAMLPadding = padding + 0.5;
387 print('---', YAMLPadding);
388 for (const [prop, value] of Object.entries(obj)) {
389 print(`${prop}: ${serialize(value)}`, YAMLPadding + 0.5);
390 }
391 print('...', YAMLPadding);
392 };
393
394 const printComment = (comment, padding = 0) => {
395 print(`# ${comment}`, padding);
396 };
397
398 const printBailOut = () => {
399 print('Bail out! Unhandled error.');
400 };
401
402 const printTestStart = (newTestMessage) => {
403 const {
404 data: { description },
405 } = newTestMessage;
406 printComment(description);
407 };
408
409 const printAssertion = (assertionMessage, { id, comment = '' }) => {
410 const { data } = assertionMessage;
411 const { pass, description } = data;
412 const label = pass === true ? 'ok' : 'not ok';
413 const directiveComment = comment ? ` # ${comment}` : '';
414 print(`${label} ${id} - ${description}` + directiveComment);
415 if (pass === false) {
416 printYAML(flatDiagnostic(data));
417 }
418 };
419
420 const printSummary = ({ success, skip, failure, total }) => {
421 print('', 0);
422 print(`1..${total}`);
423 printComment(`tests ${total}`, 0);
424 printComment(`pass ${success}`, 0);
425 printComment(`fail ${failure}`, 0);
426 printComment(`skip ${skip}`, 0);
427 };
428
429 const printHeader = () => {
430 print(`TAP version ${version}`);
431 };
432
433 return {
434 print,
435 printYAML,
436 printComment,
437 printBailOut,
438 printTestStart,
439 printAssertion,
440 printSummary,
441 printHeader,
442 };
443};
444
445const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END;
446const filterOutTestEnd = filter(isNotTestEnd);
447
448const writeMessage = ({ writer, nextId }) => {
449 const writerTable = {
450 [MESSAGE_TYPE.ASSERTION](message) {
451 return writer.printAssertion(message, { id: nextId() });
452 },
453 [MESSAGE_TYPE.TEST_START](message) {
454 if (message.data.skip) {
455 const skippedAssertionMessage = assertionMessage({
456 description: message.data.description,
457 pass: true,
458 });
459 return writer.printAssertion(skippedAssertionMessage, {
460 comment: 'SKIP',
461 id: nextId(),
462 });
463 }
464 return writer.printTestStart(message);
465 },
466 [MESSAGE_TYPE.ERROR](message) {
467 writer.printBailOut();
468 throw message.data.error;
469 },
470 };
471 return (message) => writerTable[message.type]?.(message);
472};
473
474var createTAPReporter = ({ log = defaultLogger, serialize = defaultSerializer } = {}) =>
475 async (messageStream) => {
476 const writer = createWriter({
477 log,
478 serialize,
479 });
480 const counter = createCounter();
481 const write = writeMessage({ writer, nextId: counter.nextId });
482 const stream = filterOutTestEnd(messageStream);
483
484 writer.printHeader();
485 for await (const message of stream) {
486 counter.increment(message);
487 write(message);
488 eventuallySetExitCode(message);
489 }
490 writer.printSummary(counter);
491 };
492
493var createJSONReporter = ({
494 log = defaultLogger,
495 serialize = defaultSerializer,
496} = {}) => {
497 const print = compose([log, serialize]);
498 return async (messageStream) => {
499 for await (const message of messageStream) {
500 eventuallySetExitCode(message);
501 print(message);
502 }
503 };
504};
505
506const defaultOptions = Object.freeze({ skip: false });
507const noop = () => {};
508
509const isTest = (assertionLike) =>
510 assertionLike[Symbol.asyncIterator] !== void 0;
511
512Assert.test = (description, spec, opts = defaultOptions) =>
513 test$1(description, spec, opts);
514
515Assert.skip = (description, spec, opts = defaultOptions) =>
516 test$1(description, spec, { ...opts, skip: true });
517
518Assert.only = () => {
519 throw new Error(`Can not use "only" method when not in "run only" mode`);
520};
521
522const test$1 = (description, spec, opts = defaultOptions) => {
523 const { skip = false } = opts;
524 const assertions = [];
525 let executionTime;
526 let done = false;
527 let error;
528
529 const onResult = (assertion) => {
530 if (done) {
531 throw new Error(`test "${description}"
532tried to collect an assertion after it has run to its completion.
533You might have forgotten to wait for an asynchronous task to complete
534------
535${spec.toString()}`);
536 }
537
538 assertions.push(assertion);
539 };
540
541 const specFn = skip
542 ? noop
543 : function zora_spec_fn() {
544 return spec(assertFactory({ onResult }));
545 };
546
547 const testRoutine = (async function () {
548 try {
549 const start = Date.now();
550 const result = await specFn();
551 executionTime = Date.now() - start;
552 return result;
553 } catch (e) {
554 error = e;
555 } finally {
556 done = true;
557 }
558 })();
559
560 return Object.assign(testRoutine, {
561 [Symbol.asyncIterator]: async function* () {
562 yield newTestMessage({ description, skip });
563 await testRoutine;
564 for (const assertion of assertions) {
565 if (isTest(assertion)) {
566 yield* assertion;
567 } else {
568 yield assertionMessage(assertion);
569 }
570 }
571
572 if (error) {
573 yield errorMessage({ error });
574 }
575
576 yield testEndMessage({ description, executionTime });
577 },
578 });
579};
580
581const createAssert = assertFactory;
582
583const createHarness$1 = ({ onlyMode = false } = {}) => {
584 const tests = [];
585
586 // WARNING if the "onlyMode is passed to any harness, all the harnesses will be affected.
587 // However, we do not expect multiple harnesses to be used in the same process
588 if (onlyMode) {
589 const { skip, test } = Assert;
590 Assert.test = skip;
591 Assert.only = test;
592 }
593
594 const { test, skip, only } = createAssert({
595 onResult: (test) => tests.push(test),
596 });
597
598 // for convenience
599 test.only = only;
600 test.skip = skip;
601
602 return {
603 only,
604 test,
605 skip,
606 report({ reporter }) {
607 return reporter(createMessageStream(tests));
608 },
609 };
610};
611
612async function* createMessageStream(tests) {
613 for (const test of tests) {
614 yield* test;
615 }
616}
617
618const findConfigurationValue = (name) => {
619 if (isNode) {
620 return process.env[name];
621 } else if (isDeno) {
622 return Deno.env.get(name);
623 } else if (isBrowser) {
624 return window[name];
625 }
626};
627
628const isNode = typeof process !== 'undefined';
629const isBrowser = typeof window !== 'undefined';
630const isDeno = typeof Deno !== 'undefined';
631
632let autoStart = true;
633
634const harness = createHarness$1({
635 onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0,
636});
637
638const only = harness.only;
639
640const test = harness.test;
641
642const skip = harness.skip;
643
644const report = harness.report;
645
646const hold = () => !(autoStart = false);
647
648const createHarness = (opts) => {
649 hold();
650 return createHarness$1(opts);
651};
652
653const start = async () => {
654 if (autoStart) {
655 const reporter =
656 findConfigurationValue('ZORA_REPORTER') === 'json'
657 ? createJSONReporter()
658 : createTAPReporter();
659 await report({ reporter });
660 }
661};
662
663// on next tick start reporting
664if (!isBrowser) {
665 setTimeout(start, 0);
666} else {
667 window.addEventListener('load', start);
668}
669
670export { Assert, createHarness, createJSONReporter, createTAPReporter, hold, only, report, skip, test };