UNPKG

jest-marbles

Version:

Marble testing helpers library for RxJs and Jest

470 lines (444 loc) 14.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // index.ts var index_exports = {}; __export(index_exports, { Scheduler: () => Scheduler, animate: () => animate, cold: () => cold, hot: () => hot, marbleTest: () => marbleTest, schedule: () => schedule, time: () => time }); module.exports = __toCommonJS(index_exports); // src/rxjs/cold-observable.ts var import_rxjs = require("rxjs"); // src/rxjs/scheduler.ts var import_testing = require("rxjs/testing"); // src/notification-event.ts var NotificationEvent = class { constructor(start) { this.start = start; this.marbles = ""; } get end() { return this.start + this.marbles.length; } }; // src/notification-kind.ts var ValueLiteral = {}; var NotificationKindChars = { N: ValueLiteral, C: "|" /* Completion */, E: "#" /* Error */ }; // src/marblizer.ts var frameStep = 1; var Marblizer = class _Marblizer { static marblize(messages) { const emissions = _Marblizer.getNotificationEvents(messages); let marbles = ""; for (let i = 0, prevEndFrame = 0; i < emissions.length; prevEndFrame = emissions[i].end, i++) { marbles = `${marbles}${"-" /* TimeFrame */.repeat(emissions[i].start - prevEndFrame) + emissions[i].marbles}`; } return marbles; } static marblizeSubscriptions(logs) { return logs.map( (log) => this.marblizeLogEntry(log.subscribedFrame / frameStep, "^" /* Subscription */) + this.marblizeLogEntry( (log.unsubscribedFrame - log.subscribedFrame) / frameStep - 1, "!" /* Unsubscription */ ) ); } static marblizeLogEntry(logPoint, symbol) { if (logPoint !== Infinity) { return "-" /* TimeFrame */.repeat(logPoint) + symbol; } else { return ""; } } static getNotificationEvents(messages) { const framesToEmissions = messages.reduce((result, message) => { if (!result[message.frame]) { result[message.frame] = new NotificationEvent(message.frame / frameStep); } const event = result[message.frame]; event.marbles += _Marblizer.extractMarble(message); return result; }, {}); const events = Object.values(framesToEmissions); _Marblizer.encloseGroupEvents(events); return events; } static extractMarble(message) { let marble = NotificationKindChars[message.notification.kind]; if (marble === ValueLiteral) marble = message.notification.value; return marble; } static encloseGroupEvents(events) { events.forEach((event) => { if (event.marbles.length > 1) { event.marbles = `${"(" /* GroupStart */}${event.marbles}${")" /* GroupEnd */}`; } }); } }; // src/jest/custom-matchers.ts function canMarblize(...messages) { return messages.every(isMessagesMarblizable); } function isMessagesMarblizable(messages) { return messages.every( ({ notification }) => notification.kind === "C" || notification.kind === "E" && notification.error === "error" || notification.kind === "N" && isCharacter(notification.value) ); } function isCharacter(value) { return typeof value === "string" && value.length === 1 || value !== void 0 && JSON.stringify(value).length === 1; } var customTestMatchers = { toBeNotifications(actual, expected) { let actualMarble = actual; let expectedMarble = expected; if (canMarblize(actual, expected)) { actualMarble = Marblizer.marblize(actual); expectedMarble = Marblizer.marblize(expected); } const pass = this.equals(actualMarble, expectedMarble); const message = pass ? () => this.utils.matcherHint(".not.toBeNotifications") + ` Expected notifications to not be: ${this.utils.printExpected(expectedMarble)} But got: ${this.utils.printReceived(actualMarble)}` : () => { const diffString = this.utils.diff(expectedMarble, actualMarble, { expand: true }); return this.utils.matcherHint(".toBeNotifications") + ` Expected notifications to be: ${this.utils.printExpected(expectedMarble)} But got: ${this.utils.printReceived(actualMarble)}` + (diffString ? ` Difference: ${diffString}` : ""); }; return { message, pass }; }, toBeSubscriptions(actual, expected) { const actualMarbleArray = Marblizer.marblizeSubscriptions(actual); const expectedMarbleArray = Marblizer.marblizeSubscriptions(expected); const pass = subscriptionsPass(actualMarbleArray, expectedMarbleArray); const message = pass ? () => this.utils.matcherHint(".not.toHaveSubscriptions") + ` Expected observable to not have the following subscription points: ${this.utils.printExpected(expectedMarbleArray)} But got: ${this.utils.printReceived(actualMarbleArray)}` : () => { const diffString = this.utils.diff(expectedMarbleArray, actualMarbleArray, { expand: true }); return this.utils.matcherHint(".toHaveSubscriptions") + ` Expected observable to have the following subscription points: ${this.utils.printExpected(expectedMarbleArray)} But got: ${this.utils.printReceived(actualMarbleArray)}` + (diffString ? ` Difference: ${diffString}` : ""); }; return { message, pass }; }, toHaveEmptySubscriptions(actual) { const pass = !(actual && actual.length > 0); let marbles; if (actual && actual.length > 0) { marbles = Marblizer.marblizeSubscriptions(actual); } const message = pass ? () => this.utils.matcherHint(".not.toHaveNoSubscriptions") + ` Expected observable to have at least one subscription point, but got nothing` + this.utils.printReceived("") : () => this.utils.matcherHint(".toHaveNoSubscriptions") + ` Expected observable to have no subscription points But got: ${this.utils.printReceived(marbles)} `; return { message, pass }; } }; function subscriptionsPass(actualMarbleArray, expectedMarbleArray) { if (actualMarbleArray.length !== expectedMarbleArray.length) { return false; } let pass = true; for (const actualMarble of actualMarbleArray) { if (!expectedMarbleArray.includes(actualMarble)) { pass = false; break; } } return pass; } expect.extend(customTestMatchers); // src/rxjs/assert-deep-equal.ts function expectedIsSubscriptionLogArray(actual, expected) { return actual.length === 0 && expected.length === 0 || expected.length !== 0 && expected[0].subscribedFrame !== void 0; } function actualIsSubscriptionsAndExpectedIsEmpty(actual, expected) { return expected.length === 0 && actual.length !== 0 && actual[0].subscribedFrame !== void 0; } function assertDeepEqual(actual, expected) { if (!expected) return; if (actualIsSubscriptionsAndExpectedIsEmpty(actual, expected)) { expect(actual).toHaveEmptySubscriptions(); } else if (expectedIsSubscriptionLogArray(actual, expected)) { expect(actual).toBeSubscriptions(expected); } else { expect(actual).toBeNotifications(expected); } } // src/rxjs/scheduler.ts var NEGATED = /* @__PURE__ */ Symbol("jest-marbles-negated"); var _Scheduler = class _Scheduler { static init() { const scheduler = new import_testing.TestScheduler(assertDeepEqual); scheduler.runMode = true; scheduler.maxFrames = Infinity; _Scheduler.instance = scheduler; _Scheduler.onFlush = []; _Scheduler.currentAnimate = null; _Scheduler.prevFrameTimeFactor = import_testing.TestScheduler.frameTimeFactor; import_testing.TestScheduler.frameTimeFactor = 1; } static get() { if (_Scheduler.instance) { return _Scheduler.instance; } throw new Error("Scheduler is not initialized"); } static reset() { _Scheduler.instance = null; _Scheduler.currentAnimate = null; } static flushTests() { return _Scheduler.get()["flushTests"]; } static captureAnimate(animate2) { _Scheduler.currentAnimate = animate2; } static animate(marbles) { if (!_Scheduler.currentAnimate) { throw new Error("animate() can only be used inside marbleTest()"); } _Scheduler.currentAnimate(marbles); } static markLastNegated() { const tests = _Scheduler.flushTests(); tests[tests.length - 1][NEGATED] = true; } static markLastReady() { const tests = _Scheduler.flushTests(); tests[tests.length - 1].ready = true; } static queueOnFlush(fn) { _Scheduler.onFlush.push(fn); } static clearFlushTests() { _Scheduler.flushTests().length = 0; } static installNegationAwareAssert() { const scheduler = _Scheduler.get(); const flushTests = _Scheduler.flushTests(); const hasNegated = flushTests.some((t) => t[NEGATED]); if (!hasNegated) { return; } const negatedSet = new WeakSet(flushTests.filter((t) => t[NEGATED])); const readyTests = flushTests.filter((t) => t.ready); let callIndex = 0; scheduler.assertDeepEqual = (actual, expected) => { const test = readyTests[callIndex++]; if (test && negatedSet.has(test)) { let threw = false; try { assertDeepEqual(actual, expected); } catch (e) { threw = true; } if (!threw) { throw new Error( `Expected observables to differ, but they matched. Received: ${JSON.stringify(actual)} Not expected: ${JSON.stringify(expected)}` ); } } else { assertDeepEqual(actual, expected); } }; } static teardown() { var _a; const scheduler = _Scheduler.get(); _Scheduler.installNegationAwareAssert(); scheduler.run(() => { }); while (_Scheduler.onFlush.length > 0) { (_a = _Scheduler.onFlush.shift()) == null ? void 0 : _a(); } import_testing.TestScheduler.frameTimeFactor = _Scheduler.prevFrameTimeFactor; _Scheduler.reset(); } static materializeInnerObservable(observable, outerFrame) { const scheduler = _Scheduler.get(); return scheduler.materializeInnerObservable(observable, outerFrame); } }; _Scheduler.onFlush = []; _Scheduler.currentAnimate = null; _Scheduler.prevFrameTimeFactor = 0; var Scheduler = _Scheduler; // src/rxjs/cold-observable.ts var ColdObservable = class extends import_rxjs.Observable { constructor(marbles, values, error) { super(); this.marbles = marbles; this.values = values; this.error = error; this.source = Scheduler.get().createColdObservable(marbles, values, error); } getSubscriptions() { return this.source.subscriptions; } }; // src/rxjs/hot-observable.ts var import_rxjs2 = require("rxjs"); var HotObservable = class extends import_rxjs2.Observable { constructor(marbles, values, error) { super(); this.marbles = marbles; this.values = values; this.error = error; this.source = Scheduler.get().createHotObservable(marbles, values, error); } getSubscriptions() { return this.source.subscriptions; } }; // src/rxjs/strip-alignment-chars.ts function stripAlignmentChars(marbles) { return marbles.replace(/^[ ]+/, ""); } // src/marble-test.ts function marbleTest(fn) { return () => { try { Scheduler.get().run((helpers) => { Scheduler.captureAnimate(helpers.animate); fn(); Scheduler.installNegationAwareAssert(); }); } finally { Scheduler.clearFlushTests(); } }; } // src/animate.ts function animate(marbles) { Scheduler.animate(marbles); } // index.ts function hot(marbles, values, error) { return new HotObservable(stripAlignmentChars(marbles), values, error); } function cold(marbles, values, error) { return new ColdObservable(stripAlignmentChars(marbles), values, error); } function time(marbles) { return Scheduler.get().createTime(stripAlignmentChars(marbles)); } function schedule(work, delay) { return Scheduler.get().schedule(work, delay); } var dummyPass = { message: () => "", pass: true }; var dummyFail = { message: () => "", pass: false }; expect.extend({ toHaveSubscriptions(actual, marbles) { const sanitizedMarbles = Array.isArray(marbles) ? marbles.map(stripAlignmentChars) : stripAlignmentChars(marbles); if (this.isNot) { Scheduler.get().expectSubscriptions(actual.getSubscriptions()).toBe(sanitizedMarbles); Scheduler.markLastNegated(); return dummyFail; } Scheduler.get().expectSubscriptions(actual.getSubscriptions()).toBe(sanitizedMarbles); return dummyPass; }, toHaveNoSubscriptions(actual) { if (this.isNot) { throw new Error("toHaveNoSubscriptions cannot be negated \u2014 use toHaveSubscriptions instead"); } Scheduler.get().expectSubscriptions(actual.getSubscriptions()).toBe([]); return dummyPass; }, toBeObservable(actual, expected) { if (this.isNot) { Scheduler.get().expectObservable(actual).toBe(expected.marbles, expected.values, expected.error); Scheduler.markLastNegated(); return dummyFail; } Scheduler.get().expectObservable(actual).toBe(expected.marbles, expected.values, expected.error); return dummyPass; }, toBeMarble(actual, marbles) { if (this.isNot) { Scheduler.get().expectObservable(actual).toBe(stripAlignmentChars(marbles)); Scheduler.markLastNegated(); return dummyFail; } Scheduler.get().expectObservable(actual).toBe(stripAlignmentChars(marbles)); return dummyPass; }, toSatisfyOnFlush(actual, func) { if (this.isNot) { throw new Error("toSatisfyOnFlush cannot be negated"); } Scheduler.get().expectObservable(actual); Scheduler.markLastReady(); Scheduler.queueOnFlush(func); return dummyPass; } }); beforeEach(() => { Scheduler.init(); }); afterEach(() => { Scheduler.teardown(); }); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Scheduler, animate, cold, hot, marbleTest, schedule, time }); //# sourceMappingURL=index.js.map