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
相关产品推荐
相关产品推荐

