jest-marbles
Version:
Marble testing helpers library for RxJs and Jest
124 lines (105 loc) • 3.7 kB
text/typescript
import { Observable } from 'rxjs';
import { TestMessages } from './types';
import { TestScheduler } from 'rxjs/testing';
import { assertDeepEqual } from './assert-deep-equal';
const NEGATED = Symbol('jest-marbles-negated');
export class Scheduler {
public static instance: TestScheduler | null;
private static onFlush: (() => void)[] = [];
private static currentAnimate: ((marbles: string) => void) | null = null;
private static prevFrameTimeFactor = 0;
public static init(): void {
const scheduler = new TestScheduler(assertDeepEqual);
(scheduler as any).runMode = true;
scheduler.maxFrames = Infinity;
Scheduler.instance = scheduler;
Scheduler.onFlush = [];
Scheduler.currentAnimate = null;
Scheduler.prevFrameTimeFactor = TestScheduler.frameTimeFactor;
TestScheduler.frameTimeFactor = 1;
}
public static get(): TestScheduler {
if (Scheduler.instance) {
return Scheduler.instance;
}
throw new Error('Scheduler is not initialized');
}
public static reset(): void {
Scheduler.instance = null;
Scheduler.currentAnimate = null;
}
private static flushTests(): any[] {
return (Scheduler.get() as any)['flushTests'];
}
public static captureAnimate(animate: (marbles: string) => void): void {
Scheduler.currentAnimate = animate;
}
public static animate(marbles: string): void {
if (!Scheduler.currentAnimate) {
throw new Error('animate() can only be used inside marbleTest()');
}
Scheduler.currentAnimate(marbles);
}
public static markLastNegated(): void {
const tests = Scheduler.flushTests();
tests[tests.length - 1][NEGATED] = true;
}
public static markLastReady(): void {
const tests = Scheduler.flushTests();
tests[tests.length - 1].ready = true;
}
public static queueOnFlush(fn: () => void): void {
Scheduler.onFlush.push(fn);
}
public static clearFlushTests(): void {
Scheduler.flushTests().length = 0;
}
public static installNegationAwareAssert(): void {
const scheduler = Scheduler.get();
const flushTests = Scheduler.flushTests();
const hasNegated = flushTests.some((t) => t[NEGATED]);
if (!hasNegated) {
return;
}
const negatedSet = new WeakSet<object>(flushTests.filter((t) => t[NEGATED]));
const readyTests = flushTests.filter((t) => t.ready);
let callIndex = 0;
(scheduler as any).assertDeepEqual = (actual: any, expected: any) => {
const test = readyTests[callIndex++];
if (test && negatedSet.has(test)) {
let threw = false;
try {
assertDeepEqual(actual, expected);
} catch {
threw = true;
}
if (!threw) {
throw new Error(
'Expected observables to differ, but they matched.\n' +
` Received: ${JSON.stringify(actual)}\n` +
` Not expected: ${JSON.stringify(expected)}`
);
}
} else {
assertDeepEqual(actual, expected);
}
};
}
public static teardown(): void {
const scheduler = Scheduler.get();
Scheduler.installNegationAwareAssert();
scheduler.run(() => {
/* run() forces frameTimeFactor = 1 internally; no override needed */
});
while (Scheduler.onFlush.length > 0) {
Scheduler.onFlush.shift()?.();
}
TestScheduler.frameTimeFactor = Scheduler.prevFrameTimeFactor;
Scheduler.reset();
}
public static materializeInnerObservable(observable: Observable<any>, outerFrame: number): TestMessages {
const scheduler = Scheduler.get();
// @ts-expect-error to avoid code duplication
return scheduler.materializeInnerObservable(observable, outerFrame);
}
}