import { Observable } from '../Observable'; import { Notification } from '../Notification'; import { ColdObservable } from './ColdObservable'; import { HotObservable } from './HotObservable'; import { SubscriptionLog } from './SubscriptionLog'; import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler'; import { AsyncScheduler } from '../scheduler/AsyncScheduler'; const defaultMaxFrame = 750; export class TestScheduler extends VirtualTimeScheduler { constructor(assertDeepEqual) { super(VirtualAction, defaultMaxFrame); this.assertDeepEqual = assertDeepEqual; this.hotObservables = []; this.coldObservables = []; this.flushTests = []; this.runMode = false; } createTime(marbles) { const indexOf = marbles.indexOf('|'); if (indexOf === -1) { throw new Error('marble diagram for time should have a completion marker "|"'); } return indexOf * TestScheduler.frameTimeFactor; } createColdObservable(marbles, values, error) { if (marbles.indexOf('^') !== -1) { throw new Error('cold observable cannot have subscription offset "^"'); } if (marbles.indexOf('!') !== -1) { throw new Error('cold observable cannot have unsubscription marker "!"'); } const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode); const cold = new ColdObservable(messages, this); this.coldObservables.push(cold); return cold; } createHotObservable(marbles, values, error) { if (marbles.indexOf('!') !== -1) { throw new Error('hot observable cannot have unsubscription marker "!"'); } const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode); const subject = new HotObservable(messages, this); this.hotObservables.push(subject); return subject; } materializeInnerObservable(observable, outerFrame) { const messages = []; observable.subscribe((value) => { messages.push({ frame: this.frame - outerFrame, notification: Notification.createNext(value) }); }, (err) => { messages.push({ frame: this.frame - outerFrame, notification: Notification.createError(err) }); }, () => { messages.push({ frame: this.frame - outerFrame, notification: Notification.createComplete() }); }); return messages; } expectObservable(observable, subscriptionMarbles = null) { const actual = []; const flushTest = { actual, ready: false }; const subscriptionParsed = TestScheduler.parseMarblesAsSubscriptions(subscriptionMarbles, this.runMode); const subscriptionFrame = subscriptionParsed.subscribedFrame === Number.POSITIVE_INFINITY ? 0 : subscriptionParsed.subscribedFrame; const unsubscriptionFrame = subscriptionParsed.unsubscribedFrame; let subscription; this.schedule(() => { subscription = observable.subscribe(x => { let value = x; if (x instanceof Observable) { value = this.materializeInnerObservable(value, this.frame); } actual.push({ frame: this.frame, notification: Notification.createNext(value) }); }, (err) => { actual.push({ frame: this.frame, notification: Notification.createError(err) }); }, () => { actual.push({ frame: this.frame, notification: Notification.createComplete() }); }); }, subscriptionFrame); if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) { this.schedule(() => subscription.unsubscribe(), unsubscriptionFrame); } this.flushTests.push(flushTest); const { runMode } = this; return { toBe(marbles, values, errorValue) { flushTest.ready = true; flushTest.expected = TestScheduler.parseMarbles(marbles, values, errorValue, true, runMode); } }; } expectSubscriptions(actualSubscriptionLogs) { const flushTest = { actual: actualSubscriptionLogs, ready: false }; this.flushTests.push(flushTest); const { runMode } = this; return { toBe(marbles) { const marblesArray = (typeof marbles === 'string') ? [marbles] : marbles; flushTest.ready = true; flushTest.expected = marblesArray.map(marbles => TestScheduler.parseMarblesAsSubscriptions(marbles, runMode)); } }; } flush() { const hotObservables = this.hotObservables; while (hotObservables.length > 0) { hotObservables.shift().setup(); } super.flush(); this.flushTests = this.flushTests.filter(test => { if (test.ready) { this.assertDeepEqual(test.actual, test.expected); return false; } return true; }); } static parseMarblesAsSubscriptions(marbles, runMode = false) { if (typeof marbles !== 'string') { return new SubscriptionLog(Number.POSITIVE_INFINITY); } const len = marbles.length; let groupStart = -1; let subscriptionFrame = Number.POSITIVE_INFINITY; let unsubscriptionFrame = Number.POSITIVE_INFINITY; let frame = 0; for (let i = 0; i < len; i++) { let nextFrame = frame; const advanceFrameBy = (count) => { nextFrame += count * this.frameTimeFactor; }; const c = marbles[i]; switch (c) { case ' ': if (!runMode) { advanceFrameBy(1); } break; case '-': advanceFrameBy(1); break; case '(': groupStart = frame; advanceFrameBy(1); break; case ')': groupStart = -1; advanceFrameBy(1); break; case '^': if (subscriptionFrame !== Number.POSITIVE_INFINITY) { throw new Error('found a second subscription point \'^\' in a ' + 'subscription marble diagram. There can only be one.'); } subscriptionFrame = groupStart > -1 ? groupStart : frame; advanceFrameBy(1); break; case '!': if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) { throw new Error('found a second subscription point \'^\' in a ' + 'subscription marble diagram. There can only be one.'); } unsubscriptionFrame = groupStart > -1 ? groupStart : frame; break; default: if (runMode && c.match(/^[0-9]$/)) { if (i === 0 || marbles[i - 1] === ' ') { const buffer = marbles.slice(i); const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /); if (match) { i += match[0].length - 1; const duration = parseFloat(match[1]); const unit = match[2]; let durationInMs; switch (unit) { case 'ms': durationInMs = duration; break; case 's': durationInMs = duration * 1000; break; case 'm': durationInMs = duration * 1000 * 60; break; default: break; } advanceFrameBy(durationInMs / this.frameTimeFactor); break; } } } throw new Error('there can only be \'^\' and \'!\' markers in a ' + 'subscription marble diagram. Found instead \'' + c + '\'.'); } frame = nextFrame; } if (unsubscriptionFrame < 0) { return new SubscriptionLog(subscriptionFrame); } else { return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame); } } static parseMarbles(marbles, values, errorValue, materializeInnerObservables = false, runMode = false) { if (marbles.indexOf('!') !== -1) { throw new Error('conventional marble diagrams cannot have the ' + 'unsubscription marker "!"'); } const len = marbles.length; const testMessages = []; const subIndex = runMode ? marbles.replace(/^[ ]+/, '').indexOf('^') : marbles.indexOf('^'); let frame = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor); const getValue = typeof values !== 'object' ? (x) => x : (x) => { if (materializeInnerObservables && values[x] instanceof ColdObservable) { return values[x].messages; } return values[x]; }; let groupStart = -1; for (let i = 0; i < len; i++) { let nextFrame = frame; const advanceFrameBy = (count) => { nextFrame += count * this.frameTimeFactor; }; let notification; const c = marbles[i]; switch (c) { case ' ': if (!runMode) { advanceFrameBy(1); } break; case '-': advanceFrameBy(1); break; case '(': groupStart = frame; advanceFrameBy(1); break; case ')': groupStart = -1; advanceFrameBy(1); break; case '|': notification = Notification.createComplete(); advanceFrameBy(1); break; case '^': advanceFrameBy(1); break; case '#': notification = Notification.createError(errorValue || 'error'); advanceFrameBy(1); break; default: if (runMode && c.match(/^[0-9]$/)) { if (i === 0 || marbles[i - 1] === ' ') { const buffer = marbles.slice(i); const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /); if (match) { i += match[0].length - 1; const duration = parseFloat(match[1]); const unit = match[2]; let durationInMs; switch (unit) { case 'ms': durationInMs = duration; break; case 's': durationInMs = duration * 1000; break; case 'm': durationInMs = duration * 1000 * 60; break; default: break; } advanceFrameBy(durationInMs / this.frameTimeFactor); break; } } } notification = Notification.createNext(getValue(c)); advanceFrameBy(1); break; } if (notification) { testMessages.push({ frame: groupStart > -1 ? groupStart : frame, notification }); } frame = nextFrame; } return testMessages; } run(callback) { const prevFrameTimeFactor = TestScheduler.frameTimeFactor; const prevMaxFrames = this.maxFrames; TestScheduler.frameTimeFactor = 1; this.maxFrames = Number.POSITIVE_INFINITY; this.runMode = true; AsyncScheduler.delegate = this; const helpers = { cold: this.createColdObservable.bind(this), hot: this.createHotObservable.bind(this), flush: this.flush.bind(this), expectObservable: this.expectObservable.bind(this), expectSubscriptions: this.expectSubscriptions.bind(this), }; try { const ret = callback(helpers); this.flush(); return ret; } finally { TestScheduler.frameTimeFactor = prevFrameTimeFactor; this.maxFrames = prevMaxFrames; this.runMode = false; AsyncScheduler.delegate = undefined; } } } //# sourceMappingURL=TestScheduler.js.map