My application is running Angular 17.1, and I am using Jest for my unit tests. I have the following CanActivateFn guard
allow-navi.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, createUrlTreeFromSnapshot } from '@angular/router';
import { filter, of, switchMap } from 'rxjs';
import { CoreFacade } from '@mylib/core';
export const showPensionInfoGuard: CanActivateFn = (route) => {
const coreFacade = inject(CoreFacade);
return coreFacade.allowNavigation$.pipe(
filter((status) => status !== null),
switchMap((status: boolean) => {
if (status === true) {
return of(createUrlTreeFromSnapshot(route, ['../main']));
}
return of(true);
})
);
};
This guard works as intended when used in my application, but I am having issues writing a proper unit test for the guard. I want to make sure it either allows navigation, or returns the url tree for the main route.
So far I have managed to get a test for the 'allow navigation' logic. That is when the guard just returns an observable with the value true. This happens when the CoreFacade.allowNavigation$ returns an observable that evaluates to false.
When the facade returns an observable that evaluate to true, the guard hits the if statement, and returns an observable with an UrlTree for the main route.
From what I gather, it seems that I have not correctly setup my test to allow createUrlTreeFromSnapshot to work.
So far my test looks like this:
allow-navi.guard.spec.ts
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ActivatedRoute, CanActivateFn, RouterStateSnapshot, UrlTree } from '@angular/router';
import { delay, noop, Observable, of } from 'rxjs';
import { cold } from 'jasmine-marbles';
import { RouterTestingModule } from '@angular/router/testing';
import { CoreFacade } from '@mylib/core';
import { allowNaviGuard } from './allowNavi.guard';
describe('allowNaviGuard', () => {
let facade: CoreFacade;
let route: ActivatedRoute;
const executeGuard: CanActivateFn = (...guardParameters) => TestBed.runInInjectionContext(() => allowNaviGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: CoreFacade,
useValue: {} as Partial<CoreFacade>,
},
{
provide: ActivatedRoute,
useValue: {
snapshot: {},
} as Partial<ActivatedRoute>,
},
],
});
facade = TestBed.inject(CoreFacade);
route = TestBed.inject(ActivatedRoute);
});
it('should allow navigation if no data', async () => {
// Set that we do not have pension info data
facade.allowNavigation$ = of(false);
expect(await executeGuard(route.snapshot, {} as RouterStateSnapshot)).toBeObservable(cold('(a|)', { a: true }));
});
it('should go to `main` if we have data, with toBeObservable', fakeAsync(() => {
// Set that we have pension info data
facade.allowNavigation$ = of(true);
expect(executeGuard(route.snapshot, {} as RouterStateSnapshot)).toBeObservable(cold('(a|)', { a: 'ffs' }));
}));
it('should go to `main` if we have data with subscription', fakeAsync(() => {
// Set that we have pension info data
facade.allowNavigation$ = of(true);
let response;
(executeGuard(route.snapshot, {} as RouterStateSnapshot) as Observable<UrlTree>).pipe(delay(100)).subscribe((val) => {
response = val;
});
tick(100);
expect(response).toBe(false);
}));
});
The first of the three tests works. The second, using toBeObservable, returns the following output in my console
Expected: (a|),
Received: #,
Expected:
[{"frame":0,"notification":{"kind":"N","value":"ffs"}},{"frame":0,"notification":{"kind":"C"}}]
Received:
[{"frame":0,"notification":{"kind":"E","error":{}}}],
The third test, using subscription, returns this:
TypeError: Cannot read properties of undefined (reading 'children')
11 | switchMap((status: boolean) => {
12 | if (status === true) {
> 13 | return of(createUrlTreeFromSnapshot(route, ['../main']));
| ^
14 | }
15 | return of(true);
16 | })
So it seems my setup for the test does not allow createUrlTreeFromSnapshot to work, but I cant figure out how to setup my test in order for this to actually work.
Most likely
createUrlTreeFromSnapshotdepends on the realActivatedRoute(readingchildrenon a property that does not exist in this scenario).In this scenario, I would
importRouterTestingModuleso we have a "real" instance ofActivatedRoute.Something like this:
Hopefully with the above approach you won't face
children of undefinedbecauseroute: ActivatedRouteshould have all properties.