Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spy with Jasmine on Store.select Typescript Error: Expected at least 6 arguments, but got 1. #2674

Closed
greg-md opened this issue Aug 17, 2020 · 11 comments

Comments

@greg-md
Copy link

greg-md commented Aug 17, 2020

Typescript throws an error if you want to spy on the Store class for the select method.

The problem is in the typescript declaration of the Store class which needs to be simplified.

Minimal reproduction of the bug/regression with instructions:

const storeSpy = jasmine.createSpyObj<Store>('Store', ['select']);

storeSpy.select.withArgs('foo').and.returnValue(of(null));

The above example throws Expected at least 6 arguments, but got 1.

Expected behavior:

To be able to use full typings when spying the Store class.

Versions of NgRx, Angular, Node, affected browser(s) and operating system(s):

"@ngrx/store": "^10.0.0",
@alex-okrushko
Copy link
Member

Hey @greg-md
Store testing, including how to provide spies for selectors, is described here: https://ngrx.io/guide/store/testing

Let us know if you'll have any further questions.

@greg-md
Copy link
Author

greg-md commented Aug 17, 2020

@alex-okrushko Thanks. I will reuse that, but the typescript declaration still needs to be fixed. Might affect other cases as well.

@timdeschryver
Copy link
Member

@greg-md if you create a typed store this issue should go away.

const storeSpy = jasmine.createSpyObj<Store<{foo: any}>>('Store', ['select']);

storeSpy.select.withArgs('foo').and.returnValue(of(null));

@greg-md
Copy link
Author

greg-md commented Aug 17, 2020

@timdeschryver it is not, because the problem is not in the typed store. It is in the select method declaration. We have 9 declarations of the same select method and the typescript thinks that the last one is the right one.

select<K>(mapFn: (state: T) => K): Observable<K>;
select<K, Props = any>(mapFn: (state: T, props: Props) => K, props: Props): Observable<K>;
select<a extends keyof T>(key: a): Observable<T[a]>;
select<a extends keyof T, b extends keyof T[a]>(key1: a, key2: b): Observable<T[a][b]>;
select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b]>(key1: a, key2: b, key3: c): Observable<T[a][b][c]>;
select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c]>(key1: a, key2: b, key3: c, key4: d): Observable<T[a][b][c][d]>;
select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c], e extends keyof T[a][b][c][d]>(key1: a, key2: b, key3: c, key4: d, key5: e): Observable<T[a][b][c][d][e]>;
select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c], e extends keyof T[a][b][c][d], f extends keyof T[a][b][c][d][e]>(key1: a, key2: b, key3: c, key4: d, key5: e, key6: f): Observable<T[a][b][c][d][e][f]>;
select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c], e extends keyof T[a][b][c][d], f extends keyof T[a][b][c][d][e], K = any>(key1: a, key2: b, key3: c, key4: d, key5: e, key6: f, ...paths: string[]): Observable<K>;

@timdeschryver
Copy link
Member

Exactly... and that's why you have to type the Store.
If the selected property exists in the store, it will use the the following type.

select<a extends keyof T>(key: a): Observable<T[a]>;

@greg-md
Copy link
Author

greg-md commented Aug 17, 2020

@timdeschryver Again, the problem is not in the type of the Store(I already used that, just simplified the example in the post) but in how the typescript gets the right declaration(could be a typescript issue as well, not sure).

You can try on your local. The error is on arguments count which expects to have 6 arguments.

@timdeschryver
Copy link
Member

@greg-md I did try it locally but I receive the following error

Property 'withArgs' does not exist on type '{ (mapFn: (state: object) => K): Observable; <K, Props = any>(mapFn: (state: object, props: Props) => K, props: Props): Observable; (key: a): Observable<...>; <a extends never, b extends keyof object[a]>(key1: a, key2: b): Observable<...>; <a extends never, b extends keyof object[a], c exte...'.

image

@greg-md
Copy link
Author

greg-md commented Aug 17, 2020

Might be your jasmine.crteateSpyObj is not returning the SpyObj<T>.

Screen Shot 2020-08-17 at 20 04 31

@timdeschryver
Copy link
Member

It seems like this is specific to this issue. The withArgs use the last declaration, e.g. it wont give errors if you would move the correct declaration (see my first comment) to the bottom of the select declarations.
I'm not sure how this can be solved, do you got any suggestions @greg-md?

As Alex pointed out, if you want to mock selectors see the docs.

@alex-okrushko
Copy link
Member

Closing this one. @greg-md let us know if there are any issues with the recommended approach.

@mangei
Copy link
Contributor

mangei commented Jan 23, 2023

I am facing the same issue and didn't find a good solution.

The problem arises also with toHaveBeenCalledWith (and withArgs).

My "solutions" aka workarounds so far:

  • (a) using // @ts-ignore -> my least favorite, but it works
beforeEach(() => {
        appStore = jasmine.createSpyObj(['select']);
        // @ts-ignore
        appStore.select .withArgs(AppStore.mySelector).and.returnValue(true);
    });
  • (b) using the MockStore -> not always possible/necessary
  • (c) spy on the store implementation -> spying works, but you can not just mock the whole store
describe('...', () => {
    let store: Store<AppState>;
    let storeDispatchSpy: Spy;
    let storeSelectSpy: Spy;
    ...
 
    beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
            imports: [StoreModule.forRoot(appReducers)],
            ...
        }).compileComponents();
    }));
 
    beforeEach(() => {
        store = TestBed.inject(Store);
        storeDispatchSpy = spyOn(store, 'dispatch').and.callThrough();
        storeSelectSpy = spyOn(store, 'select').and.callThrough();
        ...
    });
 
    it('...', () => {
        expect(storeDispatchSpy).toHaveBeenCalledWith(new MyAction());
        expect(storeSelectSpy).toHaveBeenCalledWith(mySelector.getData);
    });
});

I am not sure, if this is an issue of jasmine or ngrx or Typescript, but it is definitely an issue.

@greg-md Did you find a solution yet?
@timdeschryver Do you know why it uses the last declaration and does not match a valid one?

Thanks for your support!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants