# jest-marbles
[![npm version](https://badge.fury.io/js/jest-marbles.svg)](https://badge.fury.io/js/jest-marbles) ![Build Status](https://github.com/just-jeb/jest-marbles/actions/workflows/build.yml/badge.svg?branch=master) ![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)



A set of helper functions and Jest matchers for RxJs marble testing.
This library will help you to test your reactive code in easy and clear way.

# Features
 - Typescript
 - Marblized error messages

# Prerequisites
 - Jest
 - RxJs
 - Familiarity with [marbles syntax](https://rxjs.dev/guide/testing/marble-testing#marble-syntax)

# v4 migration (run mode)

v4 runs rxjs's `TestScheduler` in **run mode**:

- **Frames are 1ms each (was 10).** `time('--|')` now returns `2`, not `20`.
  Re-baseline any hard-coded frame numbers.
- **Marble diagrams are unchanged** — `cold('--a--b|')` vs `cold('--a--b|')`
  still passes; both sides shift by the same factor.
- **`TestScheduler.frameTimeFactor` is no longer honored.** Express durations
  with time-progression syntax: `cold('a 250ms b|')`.
- **Time operators use real virtual time.** For example, `debounceTime(250)` on
  `cold('a 250ms b|')` yields `cold('250ms a 1ms (b|)')` — `a` debounces out
  at 250ms and `b` flushes on completion 1ms later.
- **New `marbleTest`** for tests that need `animate()`, or that exercise
  operators using RxJS's default scheduler (`delay`, `timer`, `interval`,
  `debounceTime`, …) so they resolve against virtual time. Note: it virtualizes
  RxJS's own schedulers, **not** the global `setTimeout`/`Date.now` — raw calls
  to those in code under test still run in real time.

```js
it('drives animationFrames', marbleTest(() => {
  animate('--x--x');
  expect(source$).toBeObservable(cold('--a--(b|)'));
}));
```

# Usage

For RxJs 7:
```sh
npm i jest-marbles@latest -D
```

For RxJs 6:
```sh
npm i jest-marbles@2 -D
```

For RxJs 5:
```sh
npm i jest-marbles@1 -D
```

In the test file:

```js
import { cold, hot, time, schedule, marbleTest, animate } from 'jest-marbles';
```

Inside the test:

```js
expect(stream).toBeObservable(expected);
expect(stream).toBeMarble(marbleString);
expect(stream).toHaveSubscriptions(marbleString);
expect(stream).toHaveSubscriptions(marbleStringsArray);
expect(stream).toHaveNoSubscriptions();
expect(stream).toSatisfyOnFlush(() => {
  expect(someMock).toHaveBeenCalled();
})
```

# Examples

## toBeObservable
Verifies that the resulting stream emits certain values at certain time frames
```js
    it('Should merge two hot observables and start emitting from the subscription point', () => {
        const e1 = hot('----a--^--b-------c--|', {a: 0});
        const e2 = hot('  ---d-^--e---------f-----|', {a: 0});
        const expected = cold('---(be)----c-f-----|', {a: 0});

        expect(e1.pipe(merge(e2))).toBeObservable(expected);
    });
```
Sample output when the test fails (if change the expected result to `'-d--(be)----c-f-----|'`):
```
Expected notifications to be:
  "-d--(be)----c-f-----|"
But got:
  "---(be)----c-f-----|"
```

## toBeMarble
Same as `toBeObservable` but receives marble string instead
```js
    it('Should concatenate two cold observables into single cold observable', () => {
        const a = cold('-a-|');
        const b = cold('-b-|');
        const expected = '-a--b-|';
        expect(a.pipe(concat(b))).toBeMarble(expected);
    });
```

## toHaveSubscriptions
Verifies that the observable was subscribed in the provided time frames.
Useful, for example, when you want to verify that particular `switchMap` worked as expected:
```js
  it('Should figure out single subscription points', () => {
    const x = cold('        --a---b---c--|');
    const xsubs = '   ------^-------!';
    const y = cold('                ---d--e---f---|');
    const ysubs = '   --------------^-------------!';
    const e1 = hot('  ------x-------y------|', { x, y });
    const expected = cold('--------a---b----d--e---f---|');

    expect(e1.pipe(switchAll())).toBeObservable(expected);
    expect(x).toHaveSubscriptions(xsubs);
    expect(y).toHaveSubscriptions(ysubs);
  });
```
The matcher can also accept multiple subscription marbles:
```js
  it('Should figure out multiple subscription points', () => {
    const x = cold('                    --a---b---c--|');

    const y = cold('                ----x---x|', {x});
    const ySubscription1 = '        ----^---!';
    //                                     '--a---b---c--|'
    const ySubscription2 = '        --------^------------!';
    const expectedY = cold('        ------a---a---b---c--|');

    const z = cold('                   -x|', {x});
    //                                 '--a---b---c--|'
    const zSubscription = '            -^------------!';
    const expectedZ = cold('           ---a---b---c--|');

    expect(y.pipe(switchAll())).toBeObservable(expectedY);
    expect(z.pipe(switchAll())).toBeObservable(expectedZ);

    expect(x).toHaveSubscriptions([ySubscription1, ySubscription2, zSubscription]);
  });
```
Sample output when the test fails (if change `ySubscription1` to `'-----------------^---!'`):
```
Expected observable to have the following subscription points:
  ["-----------------^---!", "--------^------------!", "-^------------!"]
But got:
  ["-^------------!", "----^---!", "--------^------------!"]
```

## toHaveNoSubscriptions
Verifies that the observable was not subscribed during the test.
Especially useful when you want to verify that certain chain was not called due to an error:
```js
  it('Should verify that switchMap was not performed due to an error', () => {
    const x = cold('--a---b---c--|');
    const y = cold('---#-x--', {x});
    const result = y.pipe(switchAll());
    expect(result).toBeMarble('---#');
    expect(x).toHaveNoSubscriptions();
  });
```
Sample output when the test fails (if remove error and change the expected marble to `'------a---b---c--|'`):
```
Expected observable to have no subscription points
But got:
  ["----^------------!"]
```

## toSatisfyOnFlush
Allows you to assert on certain side effects/conditions that should be satisfied when the observable has been flushed (finished)
```js
  it('should verify mock has been called', () => {
      const mock = jest.fn();
      const stream$ = cold('blah|').pipe(tap(mock));
      expect(stream$).toSatisfyOnFlush(() => {
          expect(mock).toHaveBeenCalledTimes(4);
      });
  })
```

## schedule
Allows you to schedule task on specified frame
```js
  it('should verify subject values', () => {
    const source = new Subject();
    const expected = cold('ab');

    schedule(() => source.next('a'), 1);
    schedule(() => source.next('b'), 2);

    expect(source).toBeObservable(expected);
  });
```

## time-progression syntax
Use `Nms`, `Ns`, or `Nm` inside a marble string to express large durations without drawing every frame:
```js
  it('debounces emissions', marbleTest(() => {
    const src = cold('a 250ms b|');
    expect(src.pipe(debounceTime(250))).toBeObservable(cold('250ms a 1ms (b|)'));
  }));
```

## marbleTest
Wraps a test body in a real `TestScheduler.run()` context. Use this when you need `animate()`, or when the code under test uses operators backed by RxJS's default scheduler (`delay`, `timer`, `interval`, `debounceTime`, …) and you want them to resolve against virtual time. It virtualizes RxJS's schedulers only — not the global `setTimeout`/`Date.now`. Pass the return value directly to `it`:
```js
  it(
    'runs assertions inside a real run() context',
    marbleTest(() => {
      const src = cold('a 250ms b|');
      expect(src.pipe(debounceTime(250))).toBeObservable(cold('250ms a 1ms (b|)'));
    })
  );
```

## animate
Only valid inside `marbleTest`. Drives `animationFrames`-based timing by scheduling virtual animation-frame ticks at the positions marked in the marble string:
```js
  it(
    'drives animationFrames-based timing',
    marbleTest(() => {
      animate('--x--x');
      const src = animationFrames().pipe(
        map(({ elapsed }) => elapsed),
        take(2)
      );
      expect(src).toBeObservable(cold('--a--(b|)', { a: 2, b: 5 }));
    })
  );
```




