You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 11:01:24