How can I write a Jest unit test for an Angular 17 CanActivateFn guard that rely on createUrlTreeFromSnapshot?

189 Views Asked by At

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.

1

There are 1 best solutions below

0
AliF50 On

Most likely createUrlTreeFromSnapshot depends on the real ActivatedRoute (reading children on a property that does not exist in this scenario).

In this scenario, I would import RouterTestingModule so we have a "real" instance of ActivatedRoute.

Something like this:

beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      providers: [
        {
          provide: CoreFacade,
          useValue: {} as Partial<CoreFacade>,
        },
        // Remove ActivatedRoute mock
      ],
    });

    facade = TestBed.inject(CoreFacade);
    // Get a real ActivatedRoute
    route = TestBed.inject(ActivatedRoute);
  });

Hopefully with the above approach you won't face children of undefined because route: ActivatedRoute should have all properties.