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 | }
|
178 | return {
|
179 | pass,
|
180 | actual,
|
181 | expected,
|
182 | description: description,
|
183 | operator: Operator.THROWS,
|
184 | };
|
185 | };
|
186 |
|
187 | const 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 |
|
208 | const noop$1 = () => {};
|
209 |
|
210 | const hook = (onResult) => (assert) =>
|
211 | Object.fromEntries(
|
212 | Object.keys(Assert$1).map((methodName) => [
|
213 | methodName,
|
214 | (...args) => onResult(assert[methodName](...args)),
|
215 | ])
|
216 | );
|
217 |
|
218 | var 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 |
|
232 | const Assert = Assert$1;
|
233 |
|
234 | const MESSAGE_TYPE = {
|
235 | TEST_START: 'TEST_START',
|
236 | ASSERTION: 'ASSERTION',
|
237 | TEST_END: 'TEST_END',
|
238 | ERROR: 'ERROR',
|
239 | UNKNOWN: 'UNKNOWN',
|
240 | };
|
241 |
|
242 | const newTestMessage = ({ description, skip }) => ({
|
243 | type: MESSAGE_TYPE.TEST_START,
|
244 | data: { description, skip },
|
245 | });
|
246 |
|
247 | const assertionMessage = (data) => ({
|
248 | type: MESSAGE_TYPE.ASSERTION,
|
249 | data,
|
250 | });
|
251 |
|
252 | const testEndMessage = ({ description, executionTime }) => ({
|
253 | type: MESSAGE_TYPE.TEST_END,
|
254 | data: {
|
255 | description,
|
256 | executionTime,
|
257 | },
|
258 | });
|
259 |
|
260 | const errorMessage = ({ error }) => ({
|
261 | type: MESSAGE_TYPE.ERROR,
|
262 | data: {
|
263 | error,
|
264 | },
|
265 | });
|
266 |
|
267 | const isNode$1 = typeof process !== 'undefined';
|
268 |
|
269 | const flatDiagnostic = ({ pass, description, ...rest }) => rest;
|
270 |
|
271 | const 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 |
|
290 | const isObject = (candidate) =>
|
291 | typeof candidate === 'object' && candidate !== null;
|
292 |
|
293 | const stringify = (value) => JSON.stringify(value, createReplacer());
|
294 |
|
295 | const defaultSerializer = stringify;
|
296 |
|
297 | const defaultLogger = (value) => console.log(value);
|
298 |
|
299 | const isAssertionFailing = (message) =>
|
300 | message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass;
|
301 |
|
302 | const isSkipped = (message) =>
|
303 | message.type === MESSAGE_TYPE.TEST_START && message.data.skip;
|
304 |
|
305 | const eventuallySetExitCode = (message) => {
|
306 | if (isNode$1 && isAssertionFailing(message)) {
|
307 | process.exitCode = 1;
|
308 | }
|
309 | };
|
310 |
|
311 | const compose = (fns) => (arg) =>
|
312 | fns.reduceRight((arg, fn) => fn(arg), arg);
|
313 |
|
314 | const filter = (predicate) =>
|
315 | async function* (stream) {
|
316 | for await (const element of stream) {
|
317 | if (predicate(element)) {
|
318 | yield element;
|
319 | }
|
320 | }
|
321 | };
|
322 |
|
323 | const idSequence = () => {
|
324 | let id = 0;
|
325 | return () => ++id;
|
326 | };
|
327 |
|
328 | const 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 |
|
376 | const 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));
|
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 |
|
445 | const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END;
|
446 | const filterOutTestEnd = filter(isNotTestEnd);
|
447 |
|
448 | const 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 |
|
474 | var 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 |
|
493 | var 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 |
|
506 | const defaultOptions = Object.freeze({ skip: false });
|
507 | const noop = () => {};
|
508 |
|
509 | const isTest = (assertionLike) =>
|
510 | assertionLike[Symbol.asyncIterator] !== void 0;
|
511 |
|
512 | Assert.test = (description, spec, opts = defaultOptions) =>
|
513 | test$1(description, spec, opts);
|
514 |
|
515 | Assert.skip = (description, spec, opts = defaultOptions) =>
|
516 | test$1(description, spec, { ...opts, skip: true });
|
517 |
|
518 | Assert.only = () => {
|
519 | throw new Error(`Can not use "only" method when not in "run only" mode`);
|
520 | };
|
521 |
|
522 | const 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}"
|
532 | tried to collect an assertion after it has run to its completion.
|
533 | You 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 |
|
581 | const createAssert = assertFactory;
|
582 |
|
583 | const createHarness$1 = ({ onlyMode = false } = {}) => {
|
584 | const tests = [];
|
585 |
|
586 |
|
587 |
|
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 |
|
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 |
|
612 | async function* createMessageStream(tests) {
|
613 | for (const test of tests) {
|
614 | yield* test;
|
615 | }
|
616 | }
|
617 |
|
618 | const 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 |
|
628 | const isNode = typeof process !== 'undefined';
|
629 | const isBrowser = typeof window !== 'undefined';
|
630 | const isDeno = typeof Deno !== 'undefined';
|
631 |
|
632 | let autoStart = true;
|
633 |
|
634 | const harness = createHarness$1({
|
635 | onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0,
|
636 | });
|
637 |
|
638 | const only = harness.only;
|
639 |
|
640 | const test = harness.test;
|
641 |
|
642 | const skip = harness.skip;
|
643 |
|
644 | const report = harness.report;
|
645 |
|
646 | const hold = () => !(autoStart = false);
|
647 |
|
648 | const createHarness = (opts) => {
|
649 | hold();
|
650 | return createHarness$1(opts);
|
651 | };
|
652 |
|
653 | const 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 |
|
664 | if (!isBrowser) {
|
665 | setTimeout(start, 0);
|
666 | } else {
|
667 | window.addEventListener('load', start);
|
668 | }
|
669 |
|
670 | export { Assert, createHarness, createJSONReporter, createTAPReporter, hold, only, report, skip, test };
|