UNPKG

rx-sandbox

Version:

Marble diagram DSL based test suite for RxJS 6/7

215 lines 9.34 kB
"use strict"; 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