rx-sandbox
Version:
Marble diagram DSL based test suite for RxJS 6/7
215 lines • 9.34 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTestScheduler = void 0;
const rxjs_1 = require("rxjs");
const rxjs_2 = require("rxjs");
const parseObservableMarble_1 = require("../marbles/parseObservableMarble");
const SubscriptionMarbleToken_1 = require("../marbles/SubscriptionMarbleToken");
const TestMessage_1 = require("../message/TestMessage");
const coreInternalImport_1 = require("../utils/coreInternalImport");
const calculateSubscriptionFrame_1 = require("./calculateSubscriptionFrame");
/**
* Naive utility fn to determine if given object is promise.
*/
const isPromise = (obj) => !!obj && Promise.resolve(obj) == obj;
/**
* Creates `createColdObservable` function.
*/
const getCreateColdObservable = (state) => {
const { frameTimeFactor, maxFrame, scheduler } = state;
function createColdObservable(...args) {
const [marbleValue, value, error] = args;
if (typeof marbleValue === 'string' && marbleValue.indexOf(SubscriptionMarbleToken_1.SubscriptionMarbleToken.SUBSCRIBE) !== -1) {
throw new Error(`Cold observable cannot have subscription offset ${SubscriptionMarbleToken_1.SubscriptionMarbleToken.SUBSCRIBE}`);
}
const messages = Array.isArray(marbleValue)
? marbleValue
: (0, parseObservableMarble_1.parseObservableMarble)(marbleValue, value, error, false, frameTimeFactor, maxFrame);
const observable = new coreInternalImport_1.ColdObservable(messages, scheduler);
state.coldObservables.push(observable);
return observable;
}
return createColdObservable;
};
/**
* Creates `createHotObservable` function.
*/
const getCreateHotObservable = (state) => {
const { frameTimeFactor, maxFrame, scheduler } = state;
function createHotObservable(...args) {
const [marbleValue, value, error] = args;
const messages = Array.isArray(marbleValue)
? marbleValue
: (0, parseObservableMarble_1.parseObservableMarble)(marbleValue, value, error, false, frameTimeFactor, maxFrame);
const subject = new coreInternalImport_1.HotObservable(messages, scheduler);
state.hotObservables.push(subject);
return subject;
}
return createHotObservable;
};
function getSchedulerFlushFunctions(state, flushWithAsyncTick) {
const { maxFrame, autoFlush } = state;
const flushUntil = (toFrame = maxFrame) => {
if (state.flushing) {
if (flushWithAsyncTick) {
return Promise.resolve();
}
}
if (state.flushed) {
throw new Error(`Cannot schedule to get marbles, scheduler's already flushed`);
}
while (state.hotObservables.length > 0) {
state.hotObservables.shift().setup();
}
state.flushing = true;
/**
* Custom loop actions to schedule flusing actions synchronously or asynchronously based on flag.
*
* For synchronous loop, it'll use plain `while` loop. In case of flushing with tick, each action
* will be scheduled into promise instead.
*/
function loopActions(loopState, condition, fn) {
if (!flushWithAsyncTick) {
let fnResult;
while (condition(loopState)) {
fnResult = fn(loopState);
if (!!fnResult) {
break;
}
}
return fnResult;
}
else {
function loopWithTick(tickState, error) {
if (condition(tickState) && !error) {
const p = new Promise((res) => res(fn(tickState)));
return p.then((result) => loopWithTick(tickState, result));
}
else {
return Promise.resolve(error);
}
}
return loopWithTick(state);
}
}
// flush actions via custom loop fn, as same as
// https://github.com/kwonoj/rx-sandbox/blob/c2922e5c5e2503739c64af626f2861b1e1f38159/src/scheduler/TestScheduler.ts#L166-L173
const loopResult = loopActions(state, (flushState) => {
const action = flushState.scheduler.actions[0];
return !!action && action.delay <= toFrame;
}, (flushState) => {
const action = flushState.scheduler.actions.shift();
flushState.scheduler.frame = action.delay;
return action.execute(action.state, action.delay);
});
const tearDown = (error) => {
state.flushing = false;
if (toFrame >= maxFrame) {
state.flushed = true;
}
if (error) {
const { actions } = state.scheduler;
let action = null;
while ((action = actions.shift())) {
action.unsubscribe();
}
throw error;
}
};
if (isPromise(loopResult)) {
return loopResult.then((result) => tearDown(result));
}
else {
tearDown(loopResult);
}
};
const advanceTo = (toFrame) => {
if (autoFlush) {
const error = new Error('Cannot advance frame manually with autoflushing scheduler');
if (flushWithAsyncTick) {
return Promise.reject(error);
}
throw error;
}
if (toFrame < 0 || toFrame < state.scheduler.frame) {
const error = new Error(`Cannot advance frame, given frame is either negative or smaller than current frame`);
if (flushWithAsyncTick) {
return Promise.reject(error);
}
throw error;
}
const flushResult = flushUntil(toFrame);
const tearDown = () => {
state.scheduler.frame = toFrame;
};
return isPromise(flushResult) ? flushResult.then(() => tearDown()) : tearDown();
};
return { flushUntil, advanceTo };
}
function createGetMessages(state, flush) {
const { frameTimeFactor, autoFlush } = state;
const materializeInnerObservable = (observable, outerFrame) => {
const innerObservableMetadata = [];
const pushMetaData = (notification) => innerObservableMetadata.push(new TestMessage_1.TestMessageValue(state.scheduler.frame - outerFrame, notification));
observable.subscribe({
next: (value) => pushMetaData({ kind: 'N', value }),
error: (error) => pushMetaData({ kind: 'E', error }),
complete: () => pushMetaData({ kind: 'C' }),
});
return innerObservableMetadata;
};
const getMessages = (observable, unsubscriptionMarbles = null) => {
const { subscribedFrame, unsubscribedFrame } = (0, calculateSubscriptionFrame_1.calculateSubscriptionFrame)(observable, unsubscriptionMarbles, frameTimeFactor);
const observableMetadata = [];
const pushMetadata = (notification) => observableMetadata.push(new TestMessage_1.TestMessageValue(state.scheduler.frame, notification));
let subscription = null;
state.scheduler.schedule(() => {
subscription = observable.subscribe({
next: (value) => pushMetadata({
kind: 'N',
value: value instanceof rxjs_2.Observable ? materializeInnerObservable(value, state.scheduler.frame) : value,
}),
error: (error) => pushMetadata({ kind: 'E', error }),
complete: () => pushMetadata({ kind: 'C' }),
});
}, subscribedFrame);
if (unsubscribedFrame !== Number.POSITIVE_INFINITY) {
state.scheduler.schedule(() => subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe(), unsubscribedFrame);
}
const flushResult = autoFlush ? flush() : null;
if (!isPromise(flushResult)) {
return observableMetadata;
}
return flushResult.then(() => observableMetadata);
};
return getMessages;
}
const initializeSandboxState = (autoFlush, frameTimeFactor, maxFrameValue) => {
const maxFrame = maxFrameValue * frameTimeFactor;
return {
coldObservables: [],
hotObservables: [],
flushed: false,
flushing: false,
maxFrame,
frameTimeFactor,
scheduler: new rxjs_1.VirtualTimeScheduler(rxjs_1.VirtualAction, Number.POSITIVE_INFINITY),
autoFlush,
};
};
function createTestScheduler(autoFlush, frameTimeFactor, maxFrameValue, flushWithAsyncTick) {
const sandboxState = initializeSandboxState(autoFlush, frameTimeFactor, maxFrameValue);
const { flushUntil, advanceTo } = getSchedulerFlushFunctions(sandboxState, flushWithAsyncTick);
const flush = () => flushUntil();
return {
scheduler: sandboxState.scheduler,
advanceTo,
getMessages: createGetMessages(sandboxState, flush),
cold: getCreateColdObservable(sandboxState),
hot: getCreateHotObservable(sandboxState),
flush,
maxFrame: sandboxState.maxFrame,
};
}
exports.createTestScheduler = createTestScheduler;
//# sourceMappingURL=createTestScheduler.js.map