source: trip-planner-front/node_modules/rxjs/src/internal/testing/TestScheduler.ts@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 13.9 KB
Line 
1import { Observable } from '../Observable';
2import { Notification } from '../Notification';
3import { ColdObservable } from './ColdObservable';
4import { HotObservable } from './HotObservable';
5import { TestMessage } from './TestMessage';
6import { SubscriptionLog } from './SubscriptionLog';
7import { Subscription } from '../Subscription';
8import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler';
9import { AsyncScheduler } from '../scheduler/AsyncScheduler';
10
11const defaultMaxFrame: number = 750;
12
13export interface RunHelpers {
14 cold: typeof TestScheduler.prototype.createColdObservable;
15 hot: typeof TestScheduler.prototype.createHotObservable;
16 flush: typeof TestScheduler.prototype.flush;
17 expectObservable: typeof TestScheduler.prototype.expectObservable;
18 expectSubscriptions: typeof TestScheduler.prototype.expectSubscriptions;
19}
20
21interface FlushableTest {
22 ready: boolean;
23 actual?: any[];
24 expected?: any[];
25}
26
27export type observableToBeFn = (marbles: string, values?: any, errorValue?: any) => void;
28export type subscriptionLogsToBeFn = (marbles: string | string[]) => void;
29
30export class TestScheduler extends VirtualTimeScheduler {
31 public readonly hotObservables: HotObservable<any>[] = [];
32 public readonly coldObservables: ColdObservable<any>[] = [];
33 private flushTests: FlushableTest[] = [];
34 private runMode = false;
35
36 constructor(public assertDeepEqual: (actual: any, expected: any) => boolean | void) {
37 super(VirtualAction, defaultMaxFrame);
38 }
39
40 createTime(marbles: string): number {
41 const indexOf: number = marbles.indexOf('|');
42 if (indexOf === -1) {
43 throw new Error('marble diagram for time should have a completion marker "|"');
44 }
45 return indexOf * TestScheduler.frameTimeFactor;
46 }
47
48 /**
49 * @param marbles A diagram in the marble DSL. Letters map to keys in `values` if provided.
50 * @param values Values to use for the letters in `marbles`. If ommitted, the letters themselves are used.
51 * @param error The error to use for the `#` marble (if present).
52 */
53 createColdObservable<T = string>(marbles: string, values?: { [marble: string]: T }, error?: any): ColdObservable<T> {
54 if (marbles.indexOf('^') !== -1) {
55 throw new Error('cold observable cannot have subscription offset "^"');
56 }
57 if (marbles.indexOf('!') !== -1) {
58 throw new Error('cold observable cannot have unsubscription marker "!"');
59 }
60 const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
61 const cold = new ColdObservable<T>(messages, this);
62 this.coldObservables.push(cold);
63 return cold;
64 }
65
66 /**
67 * @param marbles A diagram in the marble DSL. Letters map to keys in `values` if provided.
68 * @param values Values to use for the letters in `marbles`. If ommitted, the letters themselves are used.
69 * @param error The error to use for the `#` marble (if present).
70 */
71 createHotObservable<T = string>(marbles: string, values?: { [marble: string]: T }, error?: any): HotObservable<T> {
72 if (marbles.indexOf('!') !== -1) {
73 throw new Error('hot observable cannot have unsubscription marker "!"');
74 }
75 const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
76 const subject = new HotObservable<T>(messages, this);
77 this.hotObservables.push(subject);
78 return subject;
79 }
80
81 private materializeInnerObservable(observable: Observable<any>,
82 outerFrame: number): TestMessage[] {
83 const messages: TestMessage[] = [];
84 observable.subscribe((value) => {
85 messages.push({ frame: this.frame - outerFrame, notification: Notification.createNext(value) });
86 }, (err) => {
87 messages.push({ frame: this.frame - outerFrame, notification: Notification.createError(err) });
88 }, () => {
89 messages.push({ frame: this.frame - outerFrame, notification: Notification.createComplete() });
90 });
91 return messages;
92 }
93
94 expectObservable(observable: Observable<any>,
95 subscriptionMarbles: string = null): ({ toBe: observableToBeFn }) {
96 const actual: TestMessage[] = [];
97 const flushTest: FlushableTest = { actual, ready: false };
98 const subscriptionParsed = TestScheduler.parseMarblesAsSubscriptions(subscriptionMarbles, this.runMode);
99 const subscriptionFrame = subscriptionParsed.subscribedFrame === Number.POSITIVE_INFINITY ?
100 0 : subscriptionParsed.subscribedFrame;
101 const unsubscriptionFrame = subscriptionParsed.unsubscribedFrame;
102 let subscription: Subscription;
103
104 this.schedule(() => {
105 subscription = observable.subscribe(x => {
106 let value = x;
107 // Support Observable-of-Observables
108 if (x instanceof Observable) {
109 value = this.materializeInnerObservable(value, this.frame);
110 }
111 actual.push({ frame: this.frame, notification: Notification.createNext(value) });
112 }, (err) => {
113 actual.push({ frame: this.frame, notification: Notification.createError(err) });
114 }, () => {
115 actual.push({ frame: this.frame, notification: Notification.createComplete() });
116 });
117 }, subscriptionFrame);
118
119 if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
120 this.schedule(() => subscription.unsubscribe(), unsubscriptionFrame);
121 }
122
123 this.flushTests.push(flushTest);
124 const { runMode } = this;
125
126 return {
127 toBe(marbles: string, values?: any, errorValue?: any) {
128 flushTest.ready = true;
129 flushTest.expected = TestScheduler.parseMarbles(marbles, values, errorValue, true, runMode);
130 }
131 };
132 }
133
134 expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]): ({ toBe: subscriptionLogsToBeFn }) {
135 const flushTest: FlushableTest = { actual: actualSubscriptionLogs, ready: false };
136 this.flushTests.push(flushTest);
137 const { runMode } = this;
138 return {
139 toBe(marbles: string | string[]) {
140 const marblesArray: string[] = (typeof marbles === 'string') ? [marbles] : marbles;
141 flushTest.ready = true;
142 flushTest.expected = marblesArray.map(marbles =>
143 TestScheduler.parseMarblesAsSubscriptions(marbles, runMode)
144 );
145 }
146 };
147 }
148
149 flush() {
150 const hotObservables = this.hotObservables;
151 while (hotObservables.length > 0) {
152 hotObservables.shift().setup();
153 }
154
155 super.flush();
156
157 this.flushTests = this.flushTests.filter(test => {
158 if (test.ready) {
159 this.assertDeepEqual(test.actual, test.expected);
160 return false;
161 }
162 return true;
163 });
164 }
165
166 /** @nocollapse */
167 static parseMarblesAsSubscriptions(marbles: string, runMode = false): SubscriptionLog {
168 if (typeof marbles !== 'string') {
169 return new SubscriptionLog(Number.POSITIVE_INFINITY);
170 }
171 const len = marbles.length;
172 let groupStart = -1;
173 let subscriptionFrame = Number.POSITIVE_INFINITY;
174 let unsubscriptionFrame = Number.POSITIVE_INFINITY;
175 let frame = 0;
176
177 for (let i = 0; i < len; i++) {
178 let nextFrame = frame;
179 const advanceFrameBy = (count: number) => {
180 nextFrame += count * this.frameTimeFactor;
181 };
182 const c = marbles[i];
183 switch (c) {
184 case ' ':
185 // Whitespace no longer advances time
186 if (!runMode) {
187 advanceFrameBy(1);
188 }
189 break;
190 case '-':
191 advanceFrameBy(1);
192 break;
193 case '(':
194 groupStart = frame;
195 advanceFrameBy(1);
196 break;
197 case ')':
198 groupStart = -1;
199 advanceFrameBy(1);
200 break;
201 case '^':
202 if (subscriptionFrame !== Number.POSITIVE_INFINITY) {
203 throw new Error('found a second subscription point \'^\' in a ' +
204 'subscription marble diagram. There can only be one.');
205 }
206 subscriptionFrame = groupStart > -1 ? groupStart : frame;
207 advanceFrameBy(1);
208 break;
209 case '!':
210 if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
211 throw new Error('found a second subscription point \'^\' in a ' +
212 'subscription marble diagram. There can only be one.');
213 }
214 unsubscriptionFrame = groupStart > -1 ? groupStart : frame;
215 break;
216 default:
217 // time progression syntax
218 if (runMode && c.match(/^[0-9]$/)) {
219 // Time progression must be preceeded by at least one space
220 // if it's not at the beginning of the diagram
221 if (i === 0 || marbles[i - 1] === ' ') {
222 const buffer = marbles.slice(i);
223 const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
224 if (match) {
225 i += match[0].length - 1;
226 const duration = parseFloat(match[1]);
227 const unit = match[2];
228 let durationInMs: number;
229
230 switch (unit) {
231 case 'ms':
232 durationInMs = duration;
233 break;
234 case 's':
235 durationInMs = duration * 1000;
236 break;
237 case 'm':
238 durationInMs = duration * 1000 * 60;
239 break;
240 default:
241 break;
242 }
243
244 advanceFrameBy(durationInMs / this.frameTimeFactor);
245 break;
246 }
247 }
248 }
249
250 throw new Error('there can only be \'^\' and \'!\' markers in a ' +
251 'subscription marble diagram. Found instead \'' + c + '\'.');
252 }
253
254 frame = nextFrame;
255 }
256
257 if (unsubscriptionFrame < 0) {
258 return new SubscriptionLog(subscriptionFrame);
259 } else {
260 return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame);
261 }
262 }
263
264 /** @nocollapse */
265 static parseMarbles(marbles: string,
266 values?: any,
267 errorValue?: any,
268 materializeInnerObservables: boolean = false,
269 runMode = false): TestMessage[] {
270 if (marbles.indexOf('!') !== -1) {
271 throw new Error('conventional marble diagrams cannot have the ' +
272 'unsubscription marker "!"');
273 }
274 const len = marbles.length;
275 const testMessages: TestMessage[] = [];
276 const subIndex = runMode ? marbles.replace(/^[ ]+/, '').indexOf('^') : marbles.indexOf('^');
277 let frame = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor);
278 const getValue = typeof values !== 'object' ?
279 (x: any) => x :
280 (x: any) => {
281 // Support Observable-of-Observables
282 if (materializeInnerObservables && values[x] instanceof ColdObservable) {
283 return values[x].messages;
284 }
285 return values[x];
286 };
287 let groupStart = -1;
288
289 for (let i = 0; i < len; i++) {
290 let nextFrame = frame;
291 const advanceFrameBy = (count: number) => {
292 nextFrame += count * this.frameTimeFactor;
293 };
294
295 let notification: Notification<any>;
296 const c = marbles[i];
297 switch (c) {
298 case ' ':
299 // Whitespace no longer advances time
300 if (!runMode) {
301 advanceFrameBy(1);
302 }
303 break;
304 case '-':
305 advanceFrameBy(1);
306 break;
307 case '(':
308 groupStart = frame;
309 advanceFrameBy(1);
310 break;
311 case ')':
312 groupStart = -1;
313 advanceFrameBy(1);
314 break;
315 case '|':
316 notification = Notification.createComplete();
317 advanceFrameBy(1);
318 break;
319 case '^':
320 advanceFrameBy(1);
321 break;
322 case '#':
323 notification = Notification.createError(errorValue || 'error');
324 advanceFrameBy(1);
325 break;
326 default:
327 // Might be time progression syntax, or a value literal
328 if (runMode && c.match(/^[0-9]$/)) {
329 // Time progression must be preceeded by at least one space
330 // if it's not at the beginning of the diagram
331 if (i === 0 || marbles[i - 1] === ' ') {
332 const buffer = marbles.slice(i);
333 const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
334 if (match) {
335 i += match[0].length - 1;
336 const duration = parseFloat(match[1]);
337 const unit = match[2];
338 let durationInMs: number;
339
340 switch (unit) {
341 case 'ms':
342 durationInMs = duration;
343 break;
344 case 's':
345 durationInMs = duration * 1000;
346 break;
347 case 'm':
348 durationInMs = duration * 1000 * 60;
349 break;
350 default:
351 break;
352 }
353
354 advanceFrameBy(durationInMs / this.frameTimeFactor);
355 break;
356 }
357 }
358 }
359
360 notification = Notification.createNext(getValue(c));
361 advanceFrameBy(1);
362 break;
363 }
364
365 if (notification) {
366 testMessages.push({ frame: groupStart > -1 ? groupStart : frame, notification });
367 }
368
369 frame = nextFrame;
370 }
371 return testMessages;
372 }
373
374 run<T>(callback: (helpers: RunHelpers) => T): T {
375 const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
376 const prevMaxFrames = this.maxFrames;
377
378 TestScheduler.frameTimeFactor = 1;
379 this.maxFrames = Number.POSITIVE_INFINITY;
380 this.runMode = true;
381 AsyncScheduler.delegate = this;
382
383 const helpers = {
384 cold: this.createColdObservable.bind(this),
385 hot: this.createHotObservable.bind(this),
386 flush: this.flush.bind(this),
387 expectObservable: this.expectObservable.bind(this),
388 expectSubscriptions: this.expectSubscriptions.bind(this),
389 };
390 try {
391 const ret = callback(helpers);
392 this.flush();
393 return ret;
394 } finally {
395 TestScheduler.frameTimeFactor = prevFrameTimeFactor;
396 this.maxFrames = prevMaxFrames;
397 this.runMode = false;
398 AsyncScheduler.delegate = undefined;
399 }
400 }
401}
Note: See TracBrowser for help on using the repository browser.