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

Bug: MockStore fails to properly handle overrides with memoized selector factories in store.selectSignal() #4688

Open
1 of 2 tasks
davidmccoy-8451 opened this issue Jan 27, 2025 · 0 comments

Comments

@davidmccoy-8451
Copy link

davidmccoy-8451 commented Jan 27, 2025

Which @ngrx/* package(s) are the source of the bug?

store

Minimal reproduction of the bug/regression with instructions

When using createSelector to create memoized selector factories with NgRx's MockStore, the memoization appears to break when multiple selectors are created from the same factory with different parameters.

I like to create memoized selector factories using createSelector. However, it seems to not be working as expected.

This works:

import { getRouterSelectors } from '@ngrx/router-store';
import { createSelector, Store } from '@ngrx/store';
import { inject, Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';

const selectSomething = (text: string) =>
  createSelector(
    (state: any) => state,
    state => state[text]
  );

// Define a selector factory
export const selectByText = createSelector(
  (text: string) => text,
  text => createSelector(selectSomething(text), something => something + 'more')
);

// In service:
@Injectable()
class CurrentService {
  #store = inject(Store);

  readonly something$ = this.#store.select(selectByText('param1'));
}

// In test:
describe('CurrentService', () => {
  let mockStore: MockStore;
  let service: CurrentService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CurrentService, provideMockStore()],
    });

    mockStore = TestBed.inject(MockStore);
    service = TestBed.inject(CurrentService);
  });

  it('should handle overridden selectors', done => {
    mockStore.overrideSelector(selectByText('param1'), 'value1');

    expect(mockStore.selectSignal(selectByText('param1'))()).toBe('value1');

    service.something$.subscribe(val => {
      expect(val).toBe('value1');
      done();
    });
  });
});

But weird behaviors happen when reusing the selector factory when it ostensibly shouldn't:

import { getRouterSelectors } from '@ngrx/router-store';
import { createSelector, Store } from '@ngrx/store';
import { inject, Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';

const selectSomething = (text: string) =>
  createSelector(
    (state: any) => state,
    state => state[text]
  );

// Define a selector factory
export const selectByText = createSelector(
  (text: string) => text,
  text => createSelector(selectSomething(text), something => something + 'more')
);

// In service:
@Injectable()
class CurrentService {
  #store = inject(Store);

  readonly something$ = this.#store.select(selectByText('param1'));
  readonly else$ = this.#store.select(selectByText('param2'));
}

// In test:
describe('CurrentService', () => {
  let mockStore: MockStore;
  let service: CurrentService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CurrentService, provideMockStore()],
    });

    mockStore = TestBed.inject(MockStore);
    service = TestBed.inject(CurrentService);
  });

  it('should handle overridden selectors', done => {
    mockStore.overrideSelector(selectByText('param1'), 'value1');

    expect(mockStore.selectSignal(selectByText('param1'))()).toBe('value1');

    service.something$.subscribe(val => {
      // This breaks and returns "undefinedmore"
      expect(val).toBe('value1');
      done();
    });
  });
});
import { getRouterSelectors } from '@ngrx/router-store';
import { createSelector, Store } from '@ngrx/store';
import { inject, Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';

const selectSomething = (text: string) =>
  createSelector(
    (state: any) => state,
    state => state[text]
  );

// Define a selector factory
export const selectByText = createSelector(
  (text: string) => text,
  text => createSelector(selectSomething(text), something => something + 'more')
);

// In service:
@Injectable()
class CurrentService {
  #store = inject(Store);

  readonly something$ = this.#store.select(selectByText('param1'));
  readonly else$ = this.#store.select(selectByText('param2'));
}

// In test:
describe('CurrentService', () => {
  let mockStore: MockStore;
  let service: CurrentService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CurrentService, provideMockStore()],
    });

    mockStore = TestBed.inject(MockStore);
    service = TestBed.inject(CurrentService);
  });

  it('should handle overridden selectors', done => {
    mockStore.overrideSelector(selectByText('param1'), 'value1');
    mockStore.overrideSelector(selectByText('param2'), 'value2');

    // This works
    expect(mockStore.selectSignal(selectByText('param2'))()).toBe('value2');
    // This breaks and returns "undefinedmore"
    expect(mockStore.selectSignal(selectByText('param1'))()).toBe('value1');

    service.something$.subscribe(val => {
      expect(val).toBe('value1');
      done();
    });
  });
});

I used Object.assign(selector, { random: Math.random() } in my factory to verify that createSelector is producing memoized selectors as expected. But somehow, with mock store, it's managing to break memoization.

Expected behavior

The selectors (used as selector factories) in the example should be memoized and return the expected results.

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

NgRx: 16.1.0
Angular: 16.2.11
Node: 20.18.1
Browser: N/A
OS: macOS

Other information

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No
@davidmccoy-8451 davidmccoy-8451 changed the title Bug: MockStore fails to properly handle overrides with memoized selector factories in store.select() Bug: MockStore fails to properly handle overrides with memoized selector factories in store.selectSignal() Jan 27, 2025
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

1 participant