Platform: Possibility to override a selector with an observable

Created on 3 Mar 2020  路  4Comments  路  Source: ngrx/platform

Suggestion

Currently, we can use store.overrideSelector(selector, value) and selector.setResult(value) to emit arbitrary values for a selector, which is great.

However, it would be awesome if we could also _provide an observable instead_ of calling setResult() each time we need to emit a new value.

It could be used like this: store.overrideSelectorWithObservable(selector, obs).

Why is this important?

This is a much more generic approach, similar to how we override the actions dispatched in the store. provideMockActions(() => actions) is generic enough: we can use Subjects and any other implementations of Observables.

This also plays perfectly with other testing libraries, for example rxjs-marbles: actions = m.cold('--a', {a: someAction()}). It would be great to be able to override a selector in a similar way: store.overrideSelectorWithObservable(selector, storeValues); storeValues = m.cold('--1', {'1': 'someValue'})

_Note: There might easily be a better name than overrideSelectorWithObservable :) It could probably even be just an overload of overrideSelector_

Describe any alternatives/workarounds you're currently using

Currently I simply have to refrain from using rxjs-marbles for testing functionality that depends on selectors, which is very annoying.

There might also be a way to create some kind of a helper that would listen to a source observable and, whenever any value is emitted, call selector.setResult(value); store.refreshState();. However, this code would then be reused in many places, and, IMO, this possibility should be provided by the ngrx itself.

If accepted, I would be willing to submit a PR for this feature

[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

Store

All 4 comments

Hey @Maximaximum. I've actually spent a good chunk of time looking into what you are asking for (I opened https://github.com/ngrx/platform/issues/2329 for that), but it ends being a very intrusive change.

Hi @alex-okrushko! Not sure if #2329 is actually about the same thing.

As far as I understand, your issue was about being able to push new values to an overriden selector. And it already works by using store.overrideSelector(selector, value); and then selector.setResult(newValue); store.refreshState(), so you closed that issue.

My issue, however, is about being able to use a specific pattern to do this: store.overrideSelectorWithObservable(selector, obs) and then obs.next(newValue).

If I am mistaken and we're actually talking about the same thing, then may I ask you what do you mean by an intrusive change? Will it inevitably add a breaking change? Or is it just too complex to implement?

I've created a helper function to workaround this issue:

import { MemoizedSelector, MemoizedSelectorWithProps } from '@ngrx/store';
import { MockStore } from '@ngrx/store/testing';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

export function mockSelector<TStoreState, TValue>(
    store: MockStore<TStoreState>,
    selector: MemoizedSelector<TStoreState, TValue> | MemoizedSelectorWithProps<TStoreState, any, TValue>,
    initialValue: TValue,
    selectorValues: Observable<TValue>
) {
    store.overrideSelector(selector, initialValue);

    return selectorValues.pipe(
        tap(v => {
            selector.setResult(v);
            store.refreshState();
        })
    ).subscribe();
}

Which has to be used like this:

describe('some ngrx effect', () => {
  let fooSelectorValues: Subject<FooSelectorValue>;

  beforeEach(() => {
    fooSelectorValues = new Subject();
    mockSelector(store, fooSelector, someInitialValue, fooSelectorValues);
  });

  it('should react somehow on state changes', () => {
    fooSelectorValues.next(newValue);
    // ... some expectations here ...
  });
});

However, it's very far from being perfect:

  1. There's no way to get rid of the initialValue. We need to provide an initial value yet before the effect/service/component code under test calls store.select(selector). And there is no way to delay the initial value emission. This is quite inconvenient IMHO.
  2. Therefore the selectorValues observable can only contain updates to the selector values, but not the first value to be emitted.

Closing this as we're going to keep it as is for now

Was this page helpful?
0 / 5 - 0 ratings

Related issues

brandonroberts picture brandonroberts  路  3Comments

alvipeo picture alvipeo  路  3Comments

sandangel picture sandangel  路  3Comments

NathanWalker picture NathanWalker  路  3Comments

bhaidar picture bhaidar  路  3Comments