just-jeb / jest-marbles

Helpers library for marbles testing with Jest
MIT License
111 stars 13 forks source link

toBeMarble, RangeError: Invalid count value with grouped values #274

Closed jsamr closed 2 years ago

jsamr commented 4 years ago

Although the two source streams and the result stream have all a length of 5, an exception is thrown.

Reproduction:

import { cold, hot } from 'jest-marbles'
import { buffer, mergeAll } from 'rxjs/operators'

describe('test', () => {
  it('should not fail', () => {
    const source1 = cold('AB--|')
    const source2 = hot('^-A-|')
    expect(source1.pipe(buffer(source2), mergeAll())).toBeMarble('--(AB)-|')
  })
})

Tested with version 2.5.1, node 13.5.0

just-jeb commented 4 years ago

Thanks for posting, that's quite an interesting case, took me a while to understand what's going on.
Hate to say it, but your test is invalid (in a way). Even if you want you cannot possibly write the resulting observable as marble. Here is your test without jest-marbles (pure RxJs):

  it('same test with test scheduler', () => {
    const scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
    const source1 = scheduler.createColdObservable('AB--|');
    const source2 = scheduler.createHotObservable('^-A-|');
    scheduler.expectObservable(source1.pipe(buffer(source2), mergeAll())).toBe('--(AB)-|');
    scheduler.flush();
  });

It will also fail and the reason is sync grouping feature, more precisely the way it works :

While it can be counter-intuitive at first, after all the values have synchronously emitted time will progress a number of frames equal to the number of ASCII characters in the group, including the parentheses. e.g. '(abc)' will emit the values of a, b, and c synchronously in the same frame and then advance virtual time by 5 frames, '(abc)'.length === 5. This is done because it often helps you vertically align your marble diagrams, but it's a known pain point in real-world testing.

So let's take a look at your example: Your buffer trigger subscribes at frame 0 while it is a hot observable, so it buffers A and B coming from the source observable and emits them at frame 2, then completes at frame 4. Now, since it emits them both at frame 2, they take up 4 frames together (this is how the grouping works) so in internal language of RxJs you have two events in the resulting observable:

  1. Emit A and B at frame 2
  2. Complete the observable at frame 4

But that's impossible, because the earliest frame after emitting A and B is 6 (the group (AB) takes up 4 frames, remember).

In pure RxJs test it will just fail with this error message:

 Object {
    -     "frame": 70,
    +     "frame": 40,
          "notification": Notification {
            "error": undefined,
            "hasValue": false,
            "kind": "C",
            "value": undefined,

Which means "I expected it to complete at frame 7 but it completed at frame 4".
This is cool because we just compare arrays of events.

However, when we speak of jest-marbles the process of marblization is impossible with this kind of observable. Therefore the weird error.

I do want to make such errors more explicit, but I'm not sure what is the expected error message that will at the same time give you a clue and be generic enough to cover wider range of invalid observables.
Suggestions are welcome!

jsamr commented 4 years ago

@just-jeb Thanks a lot for this thorough answer. Yeah, I just missed the main point here about marble diagrams!