Skip to content

Commit

Permalink
feat(TestScheduler): support unsubscription marbles
Browse files Browse the repository at this point in the history
Addresses issue #402
  • Loading branch information
Andre Medeiros authored and benlesh committed Sep 30, 2015
1 parent 3c3cb19 commit ffb0bb9
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 54 deletions.
49 changes: 28 additions & 21 deletions spec/schedulers/TestScheduler-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('TestScheduler', function() {
it('should exist', function () {
expect(typeof TestScheduler).toBe('function');
});

describe('parseMarbles()', function () {
it('should parse a marble string into a series of notifications and types', function () {
var result = TestScheduler.parseMarbles('-------a---b---|', { a: 'A', b: 'B' });
Expand All @@ -17,7 +17,7 @@ describe('TestScheduler', function() {
{ frame: 150, notification: Notification.createComplete() }
]);
});

it('should parse a marble string with a subscription point', function () {
var result = TestScheduler.parseMarbles('---^---a---b---|', { a: 'A', b: 'B' });
expect(result).toDeepEqual([
Expand All @@ -26,7 +26,7 @@ describe('TestScheduler', function() {
{ frame: 120, notification: Notification.createComplete() }
]);
});

it('should parse a marble string with an error', function () {
var result = TestScheduler.parseMarbles('-------a---b---#', { a: 'A', b: 'B' }, 'omg error!');
expect(result).toDeepEqual([
Expand All @@ -35,7 +35,7 @@ describe('TestScheduler', function() {
{ frame: 150, notification: Notification.createError('omg error!') }
]);
});

it('should default in the letter for the value if no value hash was passed', function(){
var result = TestScheduler.parseMarbles('--a--b--c--');
expect(result).toDeepEqual([
Expand All @@ -44,7 +44,7 @@ describe('TestScheduler', function() {
{ frame: 80, notification: Notification.createNext('c') },
])
});

it('should handle grouped values', function() {
var result = TestScheduler.parseMarbles('---(abc)---');
expect(result).toDeepEqual([
Expand All @@ -53,9 +53,9 @@ describe('TestScheduler', function() {
{ frame: 30, notification: Notification.createNext('c') }
]);
});
});

});

describe('createColdObservable()', function () {
it('should create a cold observable', function () {
var expected = ['A', 'B'];
Expand All @@ -69,7 +69,7 @@ describe('TestScheduler', function() {
expect(expected.length).toBe(0);
});
});

describe('createHotObservable()', function () {
it('should create a cold observable', function () {
var expected = ['A', 'B'];
Expand All @@ -83,19 +83,19 @@ describe('TestScheduler', function() {
expect(expected.length).toBe(0);
});
});

describe('jasmine helpers', function () {
describe('rxTestScheduler', function () {
it('should exist', function () {
expect(rxTestScheduler instanceof Rx.TestScheduler).toBe(true);
});
});

describe('cold()', function () {
it('should exist', function () {
expect(typeof cold).toBe('function');
});

it('should create a cold observable', function () {
var expected = [1, 2];
var source = cold('-a-b-|', { a: 1, b: 2 });
Expand All @@ -107,43 +107,50 @@ describe('TestScheduler', function() {
expectObservable(source).toBe('-a-b-|', { a: 1, b: 2 });
});
});

describe('hot()', function () {
it('should exist', function () {
expect(typeof hot).toBe('function');
});

it('should create a hot observable', function () {
var source = hot('---^-a-b-|', { a: 1, b: 2 });
expect(source instanceof Rx.Subject).toBe(true);
expectObservable(source).toBe('--a-b-|', { a: 1, b: 2 });
});
});

describe('expectObservable()', function () {
it('should exist', function () {
expect(typeof expectObservable).toBe('function');
});

it('should return an object with a toBe function', function () {
expect(typeof (expectObservable(Rx.Observable.of(1)).toBe)).toBe('function');
});

it('should append to flushTests array', function () {
expectObservable(Rx.Observable.empty());
expect(rxTestScheduler.flushTests.length).toBe(1);
});

it('should handle empty', function () {
expectObservable(Rx.Observable.empty()).toBe('|', {});
});

it('should handle never', function () {
expectObservable(Rx.Observable.never()).toBe('-', {});
expectObservable(Rx.Observable.never()).toBe('---', {});
});

it('should accept an unsubscription marble diagram', function () {
var source = hot('---^-a-b-|');
var unsubscribe = '---!';
var expected = '--a';
expectObservable(source, unsubscribe).toBe(expected);
});
});

describe('end-to-end helper tests', function () {
it('should be awesome', function () {
var values = { a: 1, b: 2 };
Expand All @@ -152,4 +159,4 @@ describe('TestScheduler', function() {
});
});
});
});
});
100 changes: 67 additions & 33 deletions src/schedulers/TestScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@ import VirtualTimeScheduler from './VirtualTimeScheduler';
import Notification from '../Notification';
import Subject from '../Subject';

interface FlushableTest {
observable: Observable<any>;
marbles: string;
ready: boolean;
actual?: any[];
expected?: any[];
}
export default FlushableTest;

interface SetupableTestSubject {
setup: (scheduler: TestScheduler) => void;
subject: Subject<any>;
}

export default class TestScheduler extends VirtualTimeScheduler {
private hotObservables: { setup: (scheduler: TestScheduler) => void, subject: Subject<any> }[] = [];

private setupableTestSubjects: SetupableTestSubject[] = [];
private flushTests: FlushableTest[] = [];

constructor(public assertDeepEqual: (actual: any, expected: any) => boolean | void) {
super();
}

createColdObservable(marbles: string, values?: any, error?: any) {
if (marbles.indexOf('^') !== -1) {
throw new Error('cold observable cannot have subscription offset "^"');
throw new Error('Cold observable cannot have subscription offset "^"');
}
if (marbles.indexOf('!') !== -1) {
throw new Error('Cold observable cannot have unsubscription marker "!"');
}
let messages = TestScheduler.parseMarbles(marbles, values, error);
return Observable.create(subscriber => {
Expand All @@ -23,43 +41,49 @@ export default class TestScheduler extends VirtualTimeScheduler {
}, this);
});
}

createHotObservable<T>(marbles: string, values?: any, error?: any): Subject<T> {
if (marbles.indexOf('!') !== -1) {
throw new Error('Hot observable cannot have unsubscription marker "!"');
}
let messages = TestScheduler.parseMarbles(marbles, values, error);
let subject = new Subject();
this.hotObservables.push({
this.setupableTestSubjects.push({
subject,
setup(scheduler) {
messages.forEach(({ notification, frame }) => {
scheduler.schedule(() => {
notification.observe(subject);
}, frame);
});
},
subject
}
});
return subject;
}

flushTests: ({ observable: Observable<any>, marbles: string, actual?: any[], expected?: any[], ready: boolean })[] = [];

expect(observable: Observable<any>): ({ toBe: (marbles: string, values?: any, errorValue?: any) => void }) {

expect(observable: Observable<any>,
unsubscriptionMarbles: string = null): ({ toBe: (marbles: string, values?: any, errorValue?: any) => void }) {
let actual = [];
let flushTest: ({ observable: Observable<any>, marbles: string, actual?: any[], expected?: any[], ready:boolean }) = {
observable, actual, marbles: null, ready: false
};
let flushTest: FlushableTest = { observable, actual, marbles: null, ready: false };
let unsubscriptionFrame = TestScheduler.getUnsubscriptionFrame(unsubscriptionMarbles);
let subscription;

this.schedule(() => {
observable.subscribe((value) => {
subscription = observable.subscribe((value) => {
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() });
});
}, 0);


if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
this.schedule(() => subscription.unsubscribe(), unsubscriptionFrame);
}

this.flushTests.push(flushTest);

return {
toBe(marbles: string, values?: any, errorValue?: any) {
flushTest.ready = true;
Expand All @@ -68,30 +92,41 @@ export default class TestScheduler extends VirtualTimeScheduler {
}
};
}

flush() {
const hotObservables = this.hotObservables;
while(hotObservables.length > 0) {
hotObservables.shift().setup(this);
const setupableTestSubjects = this.setupableTestSubjects;
while (setupableTestSubjects.length > 0) {
setupableTestSubjects.shift().setup(this);
}

super.flush();
const flushTests = this.flushTests.filter(test => test.ready);
while (flushTests.length > 0) {
var test = flushTests.shift();
const readyFlushTests = this.flushTests.filter(test => test.ready);
while (readyFlushTests.length > 0) {
var test = readyFlushTests.shift();
test.actual.sort((a, b) => a.frame === b.frame ? 0 : (a.frame > b.frame ? 1 : -1));
this.assertDeepEqual(test.actual, test.expected);
}
}


static getUnsubscriptionFrame(marbles?: string) {
if (typeof marbles !== 'string' || marbles.indexOf('!') === -1) {
return Number.POSITIVE_INFINITY;
}
return marbles.indexOf('!') * this.frameTimeFactor;
}

static parseMarbles(marbles: string, values?: any, errorValue?: any) : ({ frame: number, notification: Notification<any> })[] {
if (marbles.indexOf('!') !== -1) {
throw new Error('Conventional marble diagrams cannot have the ' +
'unsubscription marker "!"');
}
let len = marbles.length;
let results: ({ frame: number, notification: Notification<any> })[] = [];
let subIndex = marbles.indexOf('^');
let frameOffset = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor);
let getValue = typeof values !== 'object' ? (x) => x : (x) => values[x];
let groupStart = -1;

for (let i = 0; i < len; i++) {
let frame = i * this.frameTimeFactor;
let notification;
Expand All @@ -108,7 +143,7 @@ export default class TestScheduler extends VirtualTimeScheduler {
case '|':
notification = Notification.createComplete();
break;
case '^':
case '^':
break;
case '#':
notification = Notification.createError(errorValue || 'error');
Expand All @@ -117,10 +152,9 @@ export default class TestScheduler extends VirtualTimeScheduler {
notification = Notification.createNext(getValue(c));
break;
}



frame += frameOffset;

if (notification) {
results.push({ frame: groupStart > -1 ? groupStart : frame, notification });
}
Expand Down

0 comments on commit ffb0bb9

Please sign in to comment.