Angular 16中CanDeactivateFn守卫Karma测试失败问题
Angular 函数式路由守卫 CanDeactivateFn 测试报错解决
问题描述
我实现了一个CanDeactivateFn函数式路由守卫,用于在用户存在未保存操作时调用组件的MatDialog弹窗确认,但Karma测试时报错:TypeError: Cannot read properties of undefined (reading 'canUserExitAlertDialog')。
相关代码
守卫代码
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'; }) ); };
组件中被调用的方法
canUserExitAlertDialog(key: string): Observable<any> { //弹窗组件中有基于此key的判断逻辑 if (this.hasFormSaved) { //若表单已保存则无需确认 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'); } }
弹窗AlertComponent代码
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 console.log('do something'); } } }
测试代码
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(); }));
解决方案
报错核心原因是测试代码中的component变量未初始化,处于undefined状态,调用component.canUserExitAlertDialog时自然触发属性读取错误。不需要依赖真实组件,直接创建模拟组件(存根)即可解决,具体步骤如下:
1. 创建模拟组件(存根)
在测试文件中定义一个模拟组件,实现守卫依赖的canUserExitAlertDialog方法,返回符合要求的可观察对象:
class MockComponent { canUserExitAlertDialog(key: string): Observable<string> { return of('discard'); // 可根据测试场景修改返回值 } }
2. 初始化模拟组件实例
在beforeEach中给component变量赋值为模拟组件的实例:
beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: {} } }, ], }); // 初始化模拟组件 component = new MockComponent(); });
3. 完善测试用例,覆盖核心场景
针对守卫的逻辑,需要测试两种核心场景:用户确认放弃操作(守卫返回true)、用户取消操作(守卫返回false):
场景1:用户确认放弃,守卫返回true
it('should return true when user confirms discard', fakeAsync(() => { const activatedRoute = TestBed.inject(ActivatedRoute); const currState = {} as RouterStateSnapshot; const nextState = {} as RouterStateSnapshot; const guardResponse = TestBed.runInInjectionContext(() => { return canDeactivateGuard(component, activatedRoute.snapshot, currState, nextState) as Observable<boolean>; }); let result: boolean | undefined; guardResponse.subscribe(res => result = res); tick(); // 触发Observable完成 expect(result).toBe(true); }));
场景2:用户取消操作,守卫返回false
临时修改模拟组件的方法返回值,测试该场景:
it('should return false when user cancels', fakeAsync(() => { // 创建返回cancel的临时模拟组件 const mockComponent = new class { canUserExitAlertDialog(key: string): Observable<string> { return of('cancel'); } }(); const activatedRoute = TestBed.inject(ActivatedRoute); const currState = {} as RouterStateSnapshot; const nextState = {} as RouterStateSnapshot; const guardResponse = TestBed.runInInjectionContext(() => { return canDeactivateGuard(mockComponent, activatedRoute.snapshot, currState, nextState) as Observable<boolean>; }); let result: boolean | undefined; guardResponse.subscribe(res => result = res); tick(); expect(result).toBe(false); }));
4. (可选)模拟MatDialog行为(测试组件方法逻辑)
如果需要进一步测试组件的canUserExitAlertDialog方法(比如弹窗是否正确调用),可以在测试中提供MatDialog的模拟实现:
beforeEach(() => { const mockDialogRef = { afterClosed: () => of('discard') }; const mockDialog = { open: jasmine.createSpy('open').and.returnValue(mockDialogRef) }; TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: {} } }, { provide: MatDialog, useValue: mockDialog }, ], }); });
内容的提问来源于stack exchange,提问作者ufccoder
相关产品推荐
相关产品推荐

