I'm unit testing a handful of Angular components that can be navigated between using NGXS Store/Routing with Jest as my test framework.
- Angular v9.1.1
- Jest v9.2.2
- NGXS Router v3.7.1
- NGXS Store v3.6.2
Here's one of the tests in question:
it('should move to page 2 by clicking the next button then go back to page 1 by clicking the back icon', async () => {
let url = store.selectSnapshot(RouterState.url);
expect(url).toEqual(page1Url);
const nextButton = fixture.nativeElement.querySelector('.right-action-button');
nextButton.click();
fixture.detectChanges();
await fixture.whenStable();
url = store.selectSnapshot(RouterState.url);
expect(url).toEqual(page2Url);
const backButton = fixture.nativeElement.querySelector('.mat-icon-back');
backButton.click();
fixture.detectChanges();
await fixture.whenStable();
url = store.selectSnapshot(RouterState.url);
expect(url).toEqual(page1Url);
});
This fails at expect(url).toEqual(page2Url); so I know that nextButton is defined, however, clicking on it clearly isn't dispatching the new Store event which contains page2Url.
[EDIT] - While the button was defined, it was also disabled so the click did nothing. I adjusted the test case to ensure the button would be enabled by the time it was clicked and the value of RouterState.url is now being updated, but not to the correct value. You can see from the last code snippet that the store dispatch should cause navigation to the "side" outlet followed by "add/targets", "add/details", etc. However, in reality the result is:
Expected: "/view/(list//side:add/targets)"
Received: "/view/list"
Logging RouterState.state produces the following:
{
root: {
url: [],
params: {},
queryParams: {},
fragment: undefined,
data: {},
outlet: 'primary',
component: null,
routeConfig: null,
root: null,
parent: null,
firstChild: {
url: [],
params: {},
queryParams: {},
fragment: undefined,
data: {},
outlet: 'primary',
component: null,
routeConfig: null,
root: null,
parent: null,
firstChild: [Object],
children: [Array],
pathFromRoot: null,
paramMap: [ParamsAsMap],
queryParamMap: [ParamsAsMap],
toString: [Function]
},
children: [ [Object] ],
pathFromRoot: null,
paramMap: ParamsAsMap { params: {} },
queryParamMap: ParamsAsMap { params: {} },
toString: [Function]
},
url: '/view/list'
}
Notice how the "outlet" field is "primary" in the "firstChild" object. I'd expect this to be "side" instead based on the code shown below.
[END EDIT]
I've got some set-up in the beforeEach() that resets the value of RouterState.url in the Store back to page1Url and you can see that that's working in the first expect. Here's the set-up (imports, declarations, providers, etc. are in another beforeEach()):
beforeEach(() => {
store = TestBed.inject(Store);
store.reset({
activityView: {
creationData: {
info: {},
earthworks: {}
}
},
router: {
navigationId: 1,
state: {
root: {},
url: page1Url
},
trigger: 'none'
}
});
fixture = TestBed.createComponent(ActivityEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
In terms of the code being tested, it works as intended, but there seems to be some subtleties around testing a combination of NGXS Router/Store that I haven't quite got tied up. There also doesn't seem to be much about this sort of test either on the Angular/NGXS docs or anywhere else on Stack Overflow.
As some background on how the actual code is handling this navigation, I have a few methods in the Component that handle the various UI actions that can be taken, see below:
onCancel() {
this.store.dispatch(new SetActivityCreationData(null));
this.onClose();
}
onClose() {
this.store.dispatch(new Navigate([{ outlets: { side: null } }], null, { relativeTo: this.route.parent }));
}
onNext() {
this.page = 2;
this.store.dispatch(new Navigate([{ outlets: { side: 'add/targets' } }], null, { relativeTo: this.route.parent }));
}
onBack() {
this.page = 1;
this.store.dispatch(new Navigate([{ outlets: { side: 'add/details' } }], null, { relativeTo: this.route.parent }));
}
And again, this is working as intended but the test set-up seems to cause the relevant methods to not be called when one of the buttons is clicked.
This is getting pretty frustrating now so any guidance/suggestions would be much appreciated!