NGXS : store.reset disables dispatchers in Jasmine unit tests

Viewed 43

I am having trouble with an unexpected behavior while unit testing my NGXS store with Jasmine.

Here, I am trying to test the DeleteAlerts action :

  @Action(DeleteAlerts)
  deleteAlerts(ctx: StateContext<AlertStateModel>, action: DeleteAlerts) {
    return this.alertService.deleteAlerts(action.alertIds).pipe(
      map(response => {
          if (response.ok) {
            action.alertIds.forEach(alertId => {
              ctx.setState(patch({
                alerts: removeItem<UniqueAlert>(alert => alert.alertIds.includes(alertId))
              }));
            });
          } else {
            throw new Error('Server failed to respond.');
          }
        }
      ));
  }

but the store must first be populated with dummy data.

I created this mock :

  const alertsMock = new Alerts({
alerts: [new UniqueAlert({alertIds: ['test1']}),
  new UniqueAlert({alertIds: ['test2']}),
  new UniqueAlert({alertIds: ['test3']})]
  });

My store looks like this :

 export interface AlertStateModel {
  alerts: UniqueAlert[];
}

I then tried to populate the store with the mock :

store.reset({alerts: alertsMock.alerts})

However, when I do this in my test, the DeleteAlerts action does not dispatch when calling store.dispatch(new DeleteAlerts(alertIds))

Here is the part I do not understand : The action does dispatch when replacing the store.reset method with a GetAlerts dispatch, which is directed to load the alerts from a mocked service :

The GetAlerts action :

  @Action(GetAlerts)
  public getAlerts(ctx: StateContext<AlertStateModel>) {
    return this.alertService.getAlerts().pipe(
      tap(fetchedAlerts => {
        ctx.setState({alerts: fetchedAlerts.alerts});
      })
    );
  }

This test passes :

  it('should delete one alert from the store when DeleteAlerts is dispatched', () => {
    spyOn(alertService, 'getAlerts').and.returnValue(of(alertsMock));
    store.dispatch(new GetAlerts());
    spyOn(alertService, 'deleteAlerts').and.returnValue(of(new HttpResponse({status: 200})));
    store.dispatch(new DeleteAlerts(['test2']));
    store.selectOnce(AlertState).subscribe(data => {
      expect(data.alerts).toEqual(alertsMock.alerts.filter(alert => !alert.alertIds.includes('test2')));
    });
  });
});

This test does not :

  it('should delete one alert from the store when DeleteAlerts is dispatched', () => {
    store.reset({alerts: alertsMock.alerts});
    spyOn(alertService, 'deleteAlerts').and.returnValue(of(new HttpResponse({status: 200})));
    store.dispatch(new DeleteAlerts(['test2']));
    store.selectOnce(AlertState).subscribe(data => {
      expect(data).toEqual(alertsMock.alerts.filter(alert => !alert.alertIds.includes('test2')));
    });
  });

On top of that, you might have noticed that my expectation is on data, rather than data.alerts on the non-functional test. This is another behavior I would like to understand, since the selector should return the state, which contains a nested alerts object.

Why aren't the two tests equivalent, and why does the selector not return the expected object when using store.reset to populate the store ?

Regarding why the alertsMock contains a nested alerts object ; that is the format of the data returned by the alertService.

1 Answers

It looks like you are not expanding the store snapshot into the test reset. The documentation doesn't explicitly say to do this, but my own tests pass when I reset in this fashion.

describe('the test suite', () => {
  let store: Store;
  beforeEach(() => {
    store = TestBed.inject(Store);
    store.reset({ ...store.snapshot(),
      DESIRED_STATE
    })
  });
  
  it('must do the test', () => {
    store.reset({ ...store.snapshot(),
      NEW_DESIRED_STATE
    })
  });
});

Related