Skip to content

Commit ffb0bb9

Browse files
Andre Medeirosbenlesh
authored andcommitted
feat(TestScheduler): support unsubscription marbles
Addresses issue #402
1 parent 3c3cb19 commit ffb0bb9

File tree

2 files changed

+95
-54
lines changed

2 files changed

+95
-54
lines changed

spec/schedulers/TestScheduler-spec.js

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('TestScheduler', function() {
77
it('should exist', function () {
88
expect(typeof TestScheduler).toBe('function');
99
});
10-
10+
1111
describe('parseMarbles()', function () {
1212
it('should parse a marble string into a series of notifications and types', function () {
1313
var result = TestScheduler.parseMarbles('-------a---b---|', { a: 'A', b: 'B' });
@@ -17,7 +17,7 @@ describe('TestScheduler', function() {
1717
{ frame: 150, notification: Notification.createComplete() }
1818
]);
1919
});
20-
20+
2121
it('should parse a marble string with a subscription point', function () {
2222
var result = TestScheduler.parseMarbles('---^---a---b---|', { a: 'A', b: 'B' });
2323
expect(result).toDeepEqual([
@@ -26,7 +26,7 @@ describe('TestScheduler', function() {
2626
{ frame: 120, notification: Notification.createComplete() }
2727
]);
2828
});
29-
29+
3030
it('should parse a marble string with an error', function () {
3131
var result = TestScheduler.parseMarbles('-------a---b---#', { a: 'A', b: 'B' }, 'omg error!');
3232
expect(result).toDeepEqual([
@@ -35,7 +35,7 @@ describe('TestScheduler', function() {
3535
{ frame: 150, notification: Notification.createError('omg error!') }
3636
]);
3737
});
38-
38+
3939
it('should default in the letter for the value if no value hash was passed', function(){
4040
var result = TestScheduler.parseMarbles('--a--b--c--');
4141
expect(result).toDeepEqual([
@@ -44,7 +44,7 @@ describe('TestScheduler', function() {
4444
{ frame: 80, notification: Notification.createNext('c') },
4545
])
4646
});
47-
47+
4848
it('should handle grouped values', function() {
4949
var result = TestScheduler.parseMarbles('---(abc)---');
5050
expect(result).toDeepEqual([
@@ -53,9 +53,9 @@ describe('TestScheduler', function() {
5353
{ frame: 30, notification: Notification.createNext('c') }
5454
]);
5555
});
56-
57-
});
58-
56+
57+
});
58+
5959
describe('createColdObservable()', function () {
6060
it('should create a cold observable', function () {
6161
var expected = ['A', 'B'];
@@ -69,7 +69,7 @@ describe('TestScheduler', function() {
6969
expect(expected.length).toBe(0);
7070
});
7171
});
72-
72+
7373
describe('createHotObservable()', function () {
7474
it('should create a cold observable', function () {
7575
var expected = ['A', 'B'];
@@ -83,19 +83,19 @@ describe('TestScheduler', function() {
8383
expect(expected.length).toBe(0);
8484
});
8585
});
86-
86+
8787
describe('jasmine helpers', function () {
8888
describe('rxTestScheduler', function () {
8989
it('should exist', function () {
9090
expect(rxTestScheduler instanceof Rx.TestScheduler).toBe(true);
9191
});
9292
});
93-
93+
9494
describe('cold()', function () {
9595
it('should exist', function () {
9696
expect(typeof cold).toBe('function');
9797
});
98-
98+
9999
it('should create a cold observable', function () {
100100
var expected = [1, 2];
101101
var source = cold('-a-b-|', { a: 1, b: 2 });
@@ -107,43 +107,50 @@ describe('TestScheduler', function() {
107107
expectObservable(source).toBe('-a-b-|', { a: 1, b: 2 });
108108
});
109109
});
110-
110+
111111
describe('hot()', function () {
112112
it('should exist', function () {
113113
expect(typeof hot).toBe('function');
114114
});
115-
115+
116116
it('should create a hot observable', function () {
117117
var source = hot('---^-a-b-|', { a: 1, b: 2 });
118118
expect(source instanceof Rx.Subject).toBe(true);
119119
expectObservable(source).toBe('--a-b-|', { a: 1, b: 2 });
120120
});
121121
});
122-
122+
123123
describe('expectObservable()', function () {
124124
it('should exist', function () {
125125
expect(typeof expectObservable).toBe('function');
126126
});
127-
127+
128128
it('should return an object with a toBe function', function () {
129129
expect(typeof (expectObservable(Rx.Observable.of(1)).toBe)).toBe('function');
130130
});
131-
131+
132132
it('should append to flushTests array', function () {
133133
expectObservable(Rx.Observable.empty());
134134
expect(rxTestScheduler.flushTests.length).toBe(1);
135135
});
136-
136+
137137
it('should handle empty', function () {
138138
expectObservable(Rx.Observable.empty()).toBe('|', {});
139139
});
140-
140+
141141
it('should handle never', function () {
142142
expectObservable(Rx.Observable.never()).toBe('-', {});
143143
expectObservable(Rx.Observable.never()).toBe('---', {});
144144
});
145+
146+
it('should accept an unsubscription marble diagram', function () {
147+
var source = hot('---^-a-b-|');
148+
var unsubscribe = '---!';
149+
var expected = '--a';
150+
expectObservable(source, unsubscribe).toBe(expected);
151+
});
145152
});
146-
153+
147154
describe('end-to-end helper tests', function () {
148155
it('should be awesome', function () {
149156
var values = { a: 1, b: 2 };
@@ -152,4 +159,4 @@ describe('TestScheduler', function() {
152159
});
153160
});
154161
});
155-
});
162+
});

src/schedulers/TestScheduler.ts

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,34 @@ import VirtualTimeScheduler from './VirtualTimeScheduler';
33
import Notification from '../Notification';
44
import Subject from '../Subject';
55

6+
interface FlushableTest {
7+
observable: Observable<any>;
8+
marbles: string;
9+
ready: boolean;
10+
actual?: any[];
11+
expected?: any[];
12+
}
13+
export default FlushableTest;
14+
15+
interface SetupableTestSubject {
16+
setup: (scheduler: TestScheduler) => void;
17+
subject: Subject<any>;
18+
}
19+
620
export default class TestScheduler extends VirtualTimeScheduler {
7-
private hotObservables: { setup: (scheduler: TestScheduler) => void, subject: Subject<any> }[] = [];
8-
21+
private setupableTestSubjects: SetupableTestSubject[] = [];
22+
private flushTests: FlushableTest[] = [];
23+
924
constructor(public assertDeepEqual: (actual: any, expected: any) => boolean | void) {
1025
super();
1126
}
12-
27+
1328
createColdObservable(marbles: string, values?: any, error?: any) {
1429
if (marbles.indexOf('^') !== -1) {
15-
throw new Error('cold observable cannot have subscription offset "^"');
30+
throw new Error('Cold observable cannot have subscription offset "^"');
31+
}
32+
if (marbles.indexOf('!') !== -1) {
33+
throw new Error('Cold observable cannot have unsubscription marker "!"');
1634
}
1735
let messages = TestScheduler.parseMarbles(marbles, values, error);
1836
return Observable.create(subscriber => {
@@ -23,43 +41,49 @@ export default class TestScheduler extends VirtualTimeScheduler {
2341
}, this);
2442
});
2543
}
26-
44+
2745
createHotObservable<T>(marbles: string, values?: any, error?: any): Subject<T> {
46+
if (marbles.indexOf('!') !== -1) {
47+
throw new Error('Hot observable cannot have unsubscription marker "!"');
48+
}
2849
let messages = TestScheduler.parseMarbles(marbles, values, error);
2950
let subject = new Subject();
30-
this.hotObservables.push({
51+
this.setupableTestSubjects.push({
52+
subject,
3153
setup(scheduler) {
3254
messages.forEach(({ notification, frame }) => {
3355
scheduler.schedule(() => {
3456
notification.observe(subject);
3557
}, frame);
3658
});
37-
},
38-
subject
59+
}
3960
});
4061
return subject;
4162
}
42-
43-
flushTests: ({ observable: Observable<any>, marbles: string, actual?: any[], expected?: any[], ready: boolean })[] = [];
44-
45-
expect(observable: Observable<any>): ({ toBe: (marbles: string, values?: any, errorValue?: any) => void }) {
63+
64+
expect(observable: Observable<any>,
65+
unsubscriptionMarbles: string = null): ({ toBe: (marbles: string, values?: any, errorValue?: any) => void }) {
4666
let actual = [];
47-
let flushTest: ({ observable: Observable<any>, marbles: string, actual?: any[], expected?: any[], ready:boolean }) = {
48-
observable, actual, marbles: null, ready: false
49-
};
50-
67+
let flushTest: FlushableTest = { observable, actual, marbles: null, ready: false };
68+
let unsubscriptionFrame = TestScheduler.getUnsubscriptionFrame(unsubscriptionMarbles);
69+
let subscription;
70+
5171
this.schedule(() => {
52-
observable.subscribe((value) => {
72+
subscription = observable.subscribe((value) => {
5373
actual.push({ frame: this.frame, notification: Notification.createNext(value) });
5474
}, (err) => {
5575
actual.push({ frame: this.frame, notification: Notification.createError(err) });
5676
}, () => {
5777
actual.push({ frame: this.frame, notification: Notification.createComplete() });
5878
});
5979
}, 0);
60-
80+
81+
if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
82+
this.schedule(() => subscription.unsubscribe(), unsubscriptionFrame);
83+
}
84+
6185
this.flushTests.push(flushTest);
62-
86+
6387
return {
6488
toBe(marbles: string, values?: any, errorValue?: any) {
6589
flushTest.ready = true;
@@ -68,30 +92,41 @@ export default class TestScheduler extends VirtualTimeScheduler {
6892
}
6993
};
7094
}
71-
95+
7296
flush() {
73-
const hotObservables = this.hotObservables;
74-
while(hotObservables.length > 0) {
75-
hotObservables.shift().setup(this);
97+
const setupableTestSubjects = this.setupableTestSubjects;
98+
while (setupableTestSubjects.length > 0) {
99+
setupableTestSubjects.shift().setup(this);
76100
}
77-
101+
78102
super.flush();
79-
const flushTests = this.flushTests.filter(test => test.ready);
80-
while (flushTests.length > 0) {
81-
var test = flushTests.shift();
103+
const readyFlushTests = this.flushTests.filter(test => test.ready);
104+
while (readyFlushTests.length > 0) {
105+
var test = readyFlushTests.shift();
82106
test.actual.sort((a, b) => a.frame === b.frame ? 0 : (a.frame > b.frame ? 1 : -1));
83107
this.assertDeepEqual(test.actual, test.expected);
84108
}
85109
}
86-
110+
111+
static getUnsubscriptionFrame(marbles?: string) {
112+
if (typeof marbles !== 'string' || marbles.indexOf('!') === -1) {
113+
return Number.POSITIVE_INFINITY;
114+
}
115+
return marbles.indexOf('!') * this.frameTimeFactor;
116+
}
117+
87118
static parseMarbles(marbles: string, values?: any, errorValue?: any) : ({ frame: number, notification: Notification<any> })[] {
119+
if (marbles.indexOf('!') !== -1) {
120+
throw new Error('Conventional marble diagrams cannot have the ' +
121+
'unsubscription marker "!"');
122+
}
88123
let len = marbles.length;
89124
let results: ({ frame: number, notification: Notification<any> })[] = [];
90125
let subIndex = marbles.indexOf('^');
91126
let frameOffset = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor);
92127
let getValue = typeof values !== 'object' ? (x) => x : (x) => values[x];
93128
let groupStart = -1;
94-
129+
95130
for (let i = 0; i < len; i++) {
96131
let frame = i * this.frameTimeFactor;
97132
let notification;
@@ -108,7 +143,7 @@ export default class TestScheduler extends VirtualTimeScheduler {
108143
case '|':
109144
notification = Notification.createComplete();
110145
break;
111-
case '^':
146+
case '^':
112147
break;
113148
case '#':
114149
notification = Notification.createError(errorValue || 'error');
@@ -117,10 +152,9 @@ export default class TestScheduler extends VirtualTimeScheduler {
117152
notification = Notification.createNext(getValue(c));
118153
break;
119154
}
120-
121-
155+
122156
frame += frameOffset;
123-
157+
124158
if (notification) {
125159
results.push({ frame: groupStart > -1 ? groupStart : frame, notification });
126160
}

0 commit comments

Comments
 (0)