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

Angular嵌套确认模态框删除功能单元测试编写及报错修复

报错根因

你遇到的TypeError: Cannot read properties of null (reading 'componentInstance')由三个核心问题导致:

  • 选择器写法错误:你给modalRef.componentInstance手动赋值的css属性不会自动绑定到DOM节点,By.css("DeleteConfirmDialog")既不是合法的HTML标签名,也没有加类选择器必需的.前缀,匹配不到任何DOM元素
  • 查询范围错误:NgbModal打开的模态框默认挂载到应用根节点下,不属于PopupDialogComponent的组件子树,在popup组件的fixture作用域内查询永远找不到二次打开的确认弹窗
  • 执行顺序错误:你先执行查询确认弹窗的代码,再调用deleteCommentConfirmDialog方法触发弹窗打开,查询动作执行时弹窗还未被创建渲染,自然返回null
可直接运行的修复方案

1. 修正测试模块配置

首先在popup.component.spec.ts的测试模块配置中补全模态框依赖,否则弹窗无法正常渲染:

// 顶部引入依赖
import { NgbModalModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { fakeAsync, tick } from '@angular/core/testing';
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
import { ConfirmationDialogService } from './confirmation-dialog.service';

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [ PopupDialogComponent, ConfirmationDialogComponent ],
    imports: [ NgbModalModule ], // 必须导入ng-bootstrap模态框模块
    providers: [ 
      ConfirmationDialogService,
      NgbActiveModal
    ]
  }).compileComponents();
});

2. 编写覆盖两个要求场景的测试用例

注意NgbModal渲染的弹窗需要在触发打开逻辑后,通过fakeAsync+tick处理异步时序,再从document根节点查询元素,不要从popup组件的debugElement下查询:

it('点击Cancel按钮时不执行任何删除操作', fakeAsync(() => {
  const fixture = TestBed.createComponent(PopupDialogComponent);
  const component = fixture.componentInstance;
  // 监听删除、取消方法的调用情况
  spyOn(component, 'delete');
  spyOn(component, 'cancel');

  // 触发删除确认弹窗打开
  component.deleteCommentConfirmDialog(101);
  tick(); // 等待弹窗打开的异步逻辑执行完成
  fixture.detectChanges(); // 触发变更检测,渲染弹窗DOM

  // 查询Cancel按钮并点击(模板中Cancel按钮绑定了decline方法,类名为btn-danger)
  const cancelBtn = document.querySelector('.btn-danger') as HTMLButtonElement;
  cancelBtn.click();
  tick(); // 等待弹窗关闭的Promise回调执行
  fixture.detectChanges();

  // 断言:删除方法未被调用,取消逻辑被触发
  expect(component.delete).not.toHaveBeenCalled();
  expect(component.cancel).toHaveBeenCalled();

  // 测试完成后清理DOM中残留的弹窗节点,避免影响其他用例
  document.querySelectorAll('.modal').forEach(el => el.remove());
}));

it('点击Delete Comment按钮时成功删除对应ID的评论', fakeAsync(() => {
  const fixture = TestBed.createComponent(PopupDialogComponent);
  const component = fixture.componentInstance;
  const testCommentId = 101;
  spyOn(component, 'delete').and.callThrough();
  spyOn(component, 'cancel');

  // 触发删除确认弹窗打开
  component.deleteCommentConfirmDialog(testCommentId);
  tick();
  fixture.detectChanges();

  // 用模板里已经定义的id选择器查询删除确认按钮,最稳定不会和其他元素冲突
  const confirmBtn = document.querySelector('#delete-confirmation-id') as HTMLButtonElement;
  confirmBtn.click();
  tick();
  fixture.detectChanges();

  // 断言:删除方法被调用且传入了正确的评论ID,取消逻辑未触发
  expect(component.delete).toHaveBeenCalledOnceWith(testCommentId);
  expect(component.cancel).not.toHaveBeenCalled();

  // 清理DOM残留
  document.querySelectorAll('.modal').forEach(el => el.remove());
}));

可选优化

你之前在confirmation-dialog.service.ts中写的modalRef.componentInstance.css = "DeleteConfirmDialog"是无效逻辑,Angular不会自动把组件实例上的随机属性绑定到DOM。如果想给确认弹窗加自定义标识方便查询,可以在打开模态框时通过配置项传windowClass:

// confirmation-dialog.service.ts 修正弹窗配置
const modalRef = this.modalService.open(ConfirmationDialogComponent, { 
  size: dialogSize,
  windowClass: 'delete-confirm-dialog' // 该类会自动添加到弹窗最外层DOM节点上
});

后续查询弹窗时就可以用.delete-confirm-dialog作为选择器,避免和页面其他模态框混淆。

内容的提问来源于stack exchange,提问作者likeGreen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 07:03:16