Code coverage report for src/testing/TestScheduler.ts

Statements: 94.33% (133 / 141)      Branches: 86.44% (51 / 59)      Functions: 100% (24 / 24)      Lines: 94.07% (127 / 135)      Ignored: none     

All files » src/testing/ » TestScheduler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 2341 1 1   1 1   1                     1 1681 1681 1681   1681 1681     1 1287     1287     1287 1287 1287 1287     1 1418     1418 1418 1418 1418     175   175 175 326   44   122   175     1559 2979 1558 1558 1558   1558   1558 1558 3551   3551 175   3551   459   729       1558 137     1558   1558   1556 1556         1 1978 1978 1978   1976 1976 1976 2054           1 1694 1694 1418     1694 3536 1690 3532 3532       1 3635 1421   2214 2214 2214 2214   2214 40275 40275 40275     35645   285 285   285 285   2068       2068 2068   1992       1992 1992             2214     2214       1     7036 4296       4296 4296 4296 4296 4296 7846     2357 175   2182   4296   4296 68212 68212 68212 68212     52673   603 603   603 603   2653 2653   539   938 938   10203 10203     68212 13794     4296   1
import {Observable} from '../Observable';
import {VirtualTimeScheduler} from '../scheduler/VirtualTimeScheduler';
import {Notification} from '../Notification';
import {Subject} from '../Subject';
import {ColdObservable} from './ColdObservable';
import {HotObservable} from './HotObservable';
import {TestMessage} from './TestMessage';
import {SubscriptionLog} from './SubscriptionLog';
 
interface FlushableTest {
  ready: boolean;
  actual?: any[];
  expected?: any[];
}
 
export type observableToBeFn = (marbles: string, values?: any, errorValue?: any) => void;
export type subscriptionLogsToBeFn = (marbles: string | string[]) => void;
 
export class TestScheduler extends VirtualTimeScheduler {
  private hotObservables: HotObservable<any>[] = [];
  private coldObservables: ColdObservable<any>[] = [];
  private flushTests: FlushableTest[] = [];
 
  constructor(public assertDeepEqual: (actual: any, expected: any) => boolean | void) {
    super();
  }
 
  createColdObservable<T>(marbles: string, values?: any, error?: any): Observable<T> {
    Iif (marbles.indexOf('^') !== -1) {
      throw new Error('Cold observable cannot have subscription offset "^"');
    }
    Iif (marbles.indexOf('!') !== -1) {
      throw new Error('Cold observable cannot have unsubscription marker "!"');
    }
    const messages = TestScheduler.parseMarbles(marbles, values, error);
    const cold = new ColdObservable(messages, this);
    this.coldObservables.push(cold);
    return cold;
  }
 
  createHotObservable<T>(marbles: string, values?: any, error?: any): Subject<T> {
    Iif (marbles.indexOf('!') !== -1) {
      throw new Error('Hot observable cannot have unsubscription marker "!"');
    }
    const messages = TestScheduler.parseMarbles(marbles, values, error);
    const subject = new HotObservable(messages, this);
    this.hotObservables.push(subject);
    return subject;
  }
 
  private materializeInnerObservable(observable: Observable<any>,
                                     outerFrame: number): TestMessage[] {
    const messages: TestMessage[] = [];
    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: Observable<any>,
                   unsubscriptionMarbles: string = null): ({ toBe: observableToBeFn }) {
    const actual: TestMessage[] = [];
    const flushTest: FlushableTest = { actual, ready: false };
    const unsubscriptionFrame = TestScheduler
      .parseMarblesAsSubscriptions(unsubscriptionMarbles).unsubscribedFrame;
    let subscription;
 
    this.schedule(() => {
      subscription = observable.subscribe(x => {
        let value = x;
        // Support Observable-of-Observables
        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() });
      });
    }, 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;
        flushTest.expected = TestScheduler.parseMarbles(marbles, values, errorValue, true);
      }
    };
  }
 
  expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]): ({ toBe: subscriptionLogsToBeFn }) {
    const flushTest: FlushableTest = { actual: actualSubscriptionLogs, ready: false };
    this.flushTests.push(flushTest);
    return {
      toBe(marbles: string | string[]) {
        const marblesArray: string[] = (typeof marbles === 'string') ? [marbles] : marbles;
        flushTest.ready = true;
        flushTest.expected = marblesArray.map(marbles =>
          TestScheduler.parseMarblesAsSubscriptions(marbles)
        );
      }
    };
  }
 
  flush() {
    const hotObservables = this.hotObservables;
    while (hotObservables.length > 0) {
      hotObservables.shift().setup();
    }
 
    super.flush();
    const readyFlushTests = this.flushTests.filter(test => test.ready);
    while (readyFlushTests.length > 0) {
      const test = readyFlushTests.shift();
      this.assertDeepEqual(test.actual, test.expected);
    }
  }
 
  static parseMarblesAsSubscriptions(marbles: string): SubscriptionLog {
    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;
 
    for (let i = 0; i < len; i++) {
      const frame = i * this.frameTimeFactor;
      const c = marbles[i];
      switch (c) {
        case '-':
        case ' ':
          break;
        case '(':
          groupStart = frame;
          break;
        case ')':
          groupStart = -1;
          break;
        case '^':
          Iif (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;
          break;
        case '!':
          Iif (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:
          throw new Error('There can only be \'^\' and \'!\' markers in a ' +
            'subscription marble diagram. Found instead \'' + c + '\'.');
      }
    }
 
    Iif (unsubscriptionFrame < 0) {
      return new SubscriptionLog(subscriptionFrame);
    } else {
      return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame);
    }
  }
 
  static parseMarbles(marbles: string,
                      values?: any,
                      errorValue?: any,
                      materializeInnerObservables: boolean = false): TestMessage[] {
    Iif (marbles.indexOf('!') !== -1) {
      throw new Error('Conventional marble diagrams cannot have the ' +
        'unsubscription marker "!"');
    }
    const len = marbles.length;
    const testMessages: TestMessage[] = [];
    const subIndex = marbles.indexOf('^');
    const frameOffset = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor);
    const getValue = typeof values !== 'object' ?
      (x) => x :
      (x) => {
        // Support Observable-of-Observables
        if (materializeInnerObservables && values[x] instanceof ColdObservable) {
          return values[x].messages;
        }
        return values[x];
      };
    let groupStart = -1;
 
    for (let i = 0; i < len; i++) {
      const frame = i * this.frameTimeFactor + frameOffset;
      let notification;
      const c = marbles[i];
      switch (c) {
        case '-':
        case ' ':
          break;
        case '(':
          groupStart = frame;
          break;
        case ')':
          groupStart = -1;
          break;
        case '|':
          notification = Notification.createComplete();
          break;
        case '^':
          break;
        case '#':
          notification = Notification.createError(errorValue || 'error');
          break;
        default:
          notification = Notification.createNext(getValue(c));
          break;
      }
 
      if (notification) {
        testMessages.push({ frame: groupStart > -1 ? groupStart : frame, notification });
      }
    }
    return testMessages;
  }
}