jest-marbles
Version:
Marble testing helpers library for RxJs and Jest
470 lines (444 loc) • 14.5 kB
JavaScript
;
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