1 | const 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 |
|
12 | const specFnRegexp = /zora_spec_fn/;
|
13 | const zoraInternal = /zora\/dist/;
|
14 | const filterStackLine = (l) =>
|
15 | (l && !zoraInternal.test(l) && !l.startsWith('Error')) ||
|
16 | specFnRegexp.test(l);
|
17 |
|
18 | const 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 |
|
30 | const decorateWithLocation = (result) => {
|
31 | if (result.pass === false) {
|
32 | return {
|
33 | ...result,
|
34 | at: getAssertionLocation(),
|
35 | };
|
36 | }
|
37 | return result;
|
38 | };
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | var 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 |
|
82 | return a!==a && b!==b;
|
83 | };
|
84 |
|
85 | var eq = fastDeepEqual;
|
86 |
|
87 | const 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 |
|
99 | const 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 |
|
111 | const 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 |
|
119 | const 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 |
|
131 | const ok = (actual, description = 'should be truthy') => ({
|
132 | pass: Boolean(actual),
|
133 | actual,
|
134 | expected: 'truthy value',
|
135 | description,
|
136 | operator: Operator.OK,
|
137 | });
|
138 |
|
139 | const 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 |
|
147 | const fail = (description = 'fail called') => ({
|
148 | pass: false,
|
149 | actual: 'fail called',
|
150 | expected: 'fail not called',
|
151 | description,
|
152 | operator: Operator.FAIL,
|
153 | });
|
154 |
|
155 | const 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 |
|
190 | const 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 |
|
211 | const noop$1 = () => {};
|
212 |
|
213 | const hook = (onResult) => (assert) =>
|
214 | Object.fromEntries(
|
215 | Object.keys(Assert$1).map((methodName) => [
|
216 | methodName,
|
217 | (...args) => onResult(assert[methodName](...args)),
|
218 | ])
|
219 | );
|
220 |
|
221 | var 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 |
|
235 | const Assert = Assert$1;
|
236 |
|
237 | const MESSAGE_TYPE = {
|
238 | TEST_START: 'TEST_START',
|
239 | ASSERTION: 'ASSERTION',
|
240 | TEST_END: 'TEST_END',
|
241 | ERROR: 'ERROR',
|
242 | UNKNOWN: 'UNKNOWN',
|
243 | };
|
244 |
|
245 | const newTestMessage = ({ description, skip }) => ({
|
246 | type: MESSAGE_TYPE.TEST_START,
|
247 | data: { description, skip },
|
248 | });
|
249 |
|
250 | const assertionMessage = (data) => ({
|
251 | type: MESSAGE_TYPE.ASSERTION,
|
252 | data,
|
253 | });
|
254 |
|
255 | const testEndMessage = ({ description, executionTime }) => ({
|
256 | type: MESSAGE_TYPE.TEST_END,
|
257 | data: {
|
258 | description,
|
259 | executionTime,
|
260 | },
|
261 | });
|
262 |
|
263 | const errorMessage = ({ error }) => ({
|
264 | type: MESSAGE_TYPE.ERROR,
|
265 | data: {
|
266 | error,
|
267 | },
|
268 | });
|
269 |
|
270 | const isNode$1 = typeof process !== 'undefined';
|
271 |
|
272 | const flatDiagnostic = ({ pass, description, ...rest }) => rest;
|
273 |
|
274 | const 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 |
|
293 | const isObject = (candidate) =>
|
294 | typeof candidate === 'object' && candidate !== null;
|
295 |
|
296 | const stringify = (value) => JSON.stringify(value, createReplacer());
|
297 |
|
298 | const defaultSerializer = stringify;
|
299 |
|
300 | const defaultLogger = (value) => console.log(value);
|
301 |
|
302 | const isAssertionFailing = (message) =>
|
303 | message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass;
|
304 |
|
305 | const isSkipped = (message) =>
|
306 | message.type === MESSAGE_TYPE.TEST_START && message.data.skip;
|
307 |
|
308 | const eventuallySetExitCode = (message) => {
|
309 | if (isNode$1 && isAssertionFailing(message)) {
|
310 | process.exitCode = 1;
|
311 | }
|
312 | };
|
313 |
|
314 | const compose = (fns) => (arg) =>
|
315 | fns.reduceRight((arg, fn) => fn(arg), arg);
|
316 |
|
317 | const filter = (predicate) =>
|
318 | async function* (stream) {
|
319 | for await (const element of stream) {
|
320 | if (predicate(element)) {
|
321 | yield element;
|
322 | }
|
323 | }
|
324 | };
|
325 |
|
326 | const idSequence = () => {
|
327 | let id = 0;
|
328 | return () => ++id;
|
329 | };
|
330 |
|
331 | const 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 |
|
379 | const 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));
|
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 |
|
448 | const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END;
|
449 | const filterOutTestEnd = filter(isNotTestEnd);
|
450 |
|
451 | const 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 |
|
477 | var 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 |
|
496 | var 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 |
|
509 | const 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 |
|
519 | const isNode = typeof process !== 'undefined';
|
520 | const isBrowser = typeof window !== 'undefined';
|
521 | const isDeno = typeof Deno !== 'undefined';
|
522 |
|
523 | const DEFAULT_TIMEOUT = 5_000;
|
524 | const defaultOptions = Object.freeze({
|
525 | skip: false,
|
526 | timeout: findConfigurationValue('ZORA_TIMEOUT') || DEFAULT_TIMEOUT,
|
527 | });
|
528 | const noop = () => {};
|
529 |
|
530 | const isTest = (assertionLike) =>
|
531 | assertionLike[Symbol.asyncIterator] !== void 0;
|
532 |
|
533 | Assert.test = (description, spec, opts = defaultOptions) =>
|
534 | test$1(description, spec, opts);
|
535 |
|
536 | Assert.skip = (description, spec, opts = defaultOptions) =>
|
537 | test$1(description, spec, { ...opts, skip: true });
|
538 |
|
539 | Assert.only = () => {
|
540 | throw new Error(`Can not use "only" method when not in "run only" mode`);
|
541 | };
|
542 |
|
543 | const 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 |
|
552 | const 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}"
|
562 | tried to collect an assertion after it has run to its completion.
|
563 | You 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 |
|
621 | const createAssert = assertFactory;
|
622 |
|
623 | const createHarness$1 = ({ onlyMode = false } = {}) => {
|
624 | const tests = [];
|
625 |
|
626 |
|
627 |
|
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 |
|
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 |
|
652 | async function* createMessageStream(tests) {
|
653 | for (const test of tests) {
|
654 | yield* test;
|
655 | }
|
656 | }
|
657 |
|
658 | let autoStart = true;
|
659 |
|
660 | const harness = createHarness$1({
|
661 | onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0,
|
662 | });
|
663 |
|
664 | const only = harness.only;
|
665 |
|
666 | const test = harness.test;
|
667 |
|
668 | const skip = harness.skip;
|
669 |
|
670 | const report = harness.report;
|
671 |
|
672 | const hold = () => !(autoStart = false);
|
673 |
|
674 | const createHarness = (opts) => {
|
675 | hold();
|
676 | return createHarness$1(opts);
|
677 | };
|
678 |
|
679 | const 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 |
|
690 | if (!isBrowser) {
|
691 | setTimeout(start, 0);
|
692 | } else {
|
693 | window.addEventListener('load', start);
|
694 | }
|
695 |
|
696 | export { Assert, createHarness, createJSONReporter, createTAPReporter, hold, only, report, skip, test };
|