Search code examples
angularangular-router-guardsangular16angular-route-guardscandeactivate

unable to perform CanDeactivateFn Karma Test Angular 16


I have a canDeactivateGuard which returns from component's MatDialog for unsaved action. My problem is I am unable to test the functional guard and getting error - TypeError: Cannot read properties of undefined (reading 'canUserExitAlertDialog')

Here is the Guard-

import { CanDeactivateFn } from '@angular/router';
import { Observable, map } from 'rxjs';

export const canDeactivateGuard: CanDeactivateFn<any> = (
  component,
  route,
  state
): Observable<boolean> | boolean => {
  return component.canUserExitAlertDialog('back').pipe(
    map((result) => {
      console.log(result);
      return result === 'discard';
    })
  );
};

Here is my component's method which the guard is calling- Note: I have a mat-dialog which have two buttons - 'discard' and 'cancel'. On 'discard' click user is redirected to home page.

canUserExitAlertDialog(key: string): Observable<any> { 
//I have a condition in the alert component based on this key
    if (this.hasFormSaved) { //if any changes are saved then not considered
      return of('discard');
    }
    const dialogConfig = new MatDialogConfig();
    dialogConfig.disableClose = true;
    dialogConfig.autoFocus = true;
    dialogConfig.data = { action: key, redirect: 'home' };
    if (this.wasFormChanged || this.form.dirty) {
      const dialogRef = this.dialog.open(AlertComponent, dialogConfig);
      return dialogRef.afterClosed();
    } else {
      this.dialog.closeAll();
      return of('discard');
    }
  }

Code in Dialog Alert component:

export class AlertComponent {
  userAction!: any;
  alertMsg = 'Are you sure you want to discard the changes?';
  unsavedChanges = 'This will reload the page';
  constructor(
    @Inject(MAT_DIALOG_DATA) data: any,
    @Inject(Window) private window: Window,
    private dialogRef: MatDialogRef<AlertComponent>
  ) {
    this.userAction = data;
  }

  public onCancel(): void {
    this.dialogRef.close('cancel');
  }

  public onDiscard(): void {
    this.dialogRef.close('discard');
    if (this.userAction.action === 'something') { //key I passed from the main component
      console.log('do something');
   }
  }
}

Finally here is my code in CanDeactivate spec file-

describe('canDeactivateGuard functional guard', () => {
  let nextState: RouterStateSnapshot;
  let component: MyComponent;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: {},
          },
        },
      ],
    });
  });

  it('should be created', fakeAsync(() => {
    const activatedRoute = TestBed.inject(ActivatedRoute);
    const nextState = {} as RouterStateSnapshot;
    const currState = {} as RouterStateSnapshot;
    const guardResponse = TestBed.runInInjectionContext(() => {
      canDeactivateGuard(
        component,
        activatedRoute.snapshot,
        currState,
        nextState
      ) as Observable<any>;
    });
    expect(guardResponse).toBeTruthy();
  }));

I have tried to create a stub component and define the canUserExitAlertDialog method but didn't help. Is there another way to do this test successfully? AS per angular, class level deactivate guard is deprecated.

Error here- error message

Test Coverage- enter image description here


Solution

  • We can also mock the component to return an observable, then use fakeAsync and flush to receive the response

      it('should be created', fakeAsync(() => {
        const activatedRoute = TestBed.inject(ActivatedRoute);
        const nextState = {} as RouterStateSnapshot;
        const currState = {} as RouterStateSnapshot
        let output;
        const guardResponse = TestBed.runInInjectionContext(() => {
          canDeactivateGuard(
            { canUserExitAlertDialog: (param) => of('discard'), } as any, // <- changed here!
            activatedRoute.snapshot,
            currState,
            nextState
          ).subscribe((data) => {
            output = data;
         });
        });
        flush();
        expect(output).toBeTruthy();
      }));
    

    Since you are not initializing the component you are getting this error. There are two ways to atleast move forward.

    Try initializing the component as an instance and check if the tests are passing!

    it('should be created', fakeAsync(() => {
        const activatedRoute = TestBed.inject(ActivatedRoute);
        const nextState = {} as RouterStateSnapshot;
        const currState = {} as RouterStateSnapshot;
        const guardResponse = TestBed.runInInjectionContext(() => {
          canDeactivateGuard(
            new AlertComponent(), // <- changed here!
            activatedRoute.snapshot,
            currState,
            nextState
          ) as Observable<any>;
        });
        expect(guardResponse).toBeTruthy();
      }));
    

    Add the component to the testbed, create the component and inject it!

    it('should be created', fakeAsync(() => {
        const activatedRoute = TestBed.inject(ActivatedRoute);
        const nextState = {} as RouterStateSnapshot;
        const currState = {} as RouterStateSnapshot;
        const fixture = TestBed.createComponent(FeedbackCardComponent); // <- changed here!
        component = fixture.componentInstance; // <- changed here!
        const guardResponse = TestBed.runInInjectionContext(() => {
          canDeactivateGuard(
            component,
            activatedRoute.snapshot,
            currState,
            nextState
          ) as Observable<any>;
        });
        expect(guardResponse).toBeTruthy();
      }));