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
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();
}));