Angular中使用Jasmine测试Ionic Modal无法访问内部组件问题
业务实现代码
宿主页面打开Modal、传参、接收回传数据的核心代码如下:
const modal = await this.modalController.create({ component: EditPositionModalComponent, componentProps: {orderPosition} }); await modal.present(); // 等待Modal关闭,处理回传数据 let {data} = await modal.onDidDismiss();
测试目标
本次集成测试需要覆盖两个验证点:
- 传入Modal的
orderPosition初始数据准确无误 - Modal关闭后,用户编辑后的修改数据可正确回传至宿主页面
现有测试代码与问题
当前已编写的测试基础代码如下:
describe('OrderDetailPage/EditPosition Integrationtest', async () => { let component: OrderDetailPage; let fixture: ComponentFixture<OrderDetailPage>; let modalControllerSpy : Spy; beforeEach((() => { TestBed.configureTestingModule({ declarations: [OrderDetailPage], schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [ CommonModule, IonicModule.forRoot(), HttpClientTestingModule, ReactiveComponentModule, ReactiveFormsModule, ComponentsModule, RouterTestingModule, TranslateModule.forRoot()], providers: [ provideMockStore({}), ] }).compileComponents(); fixture = TestBed.createComponent(OrderDetailPage); component = fixture.componentInstance; fixture.detectChanges(); modalControllerSpy = spyOn(ModalController.prototype, 'create').and.callThrough(); component.onEditPosition(new MouseEvent(''), testOrderPosition); // 触发打开Modal的点击事件 fixture.detectChanges(); })); it('should send correct data to modal', async () => { let modal: HTMLIonModalElement = await modalControllerSpy.calls.first().returnValue; await modal.present(); }); });
目前测试中可通过spy获取modalController创建的HTMLIonModalElement实例,但始终无法访问Modal承载的EditPositionModalComponent组件实例。即使等待modal.present()执行完成、使用fakeAsync的tick方法推进异步任务队列,Modal内部依然没有可用于测试的有效内容,当前获取到的Modal DOM结构如下:
<ion-modal class="md modal-default hydrated" id="ion-overlay-1" no-router="" aria-modal="true" tabindex="-1" style="z-index: 20001;"> <app-edit-position-modal _nghost-a-c317="" class="ion-page"> <!--container--><!--container--> </app-edit-position-modal> <app-edit-position-modal _nghost-a-c317="" class="ion-page"> <!--container--><!--container--> </app-edit-position-modal> </ion-modal>
解决方案
1. 补全TestBed模块配置
当前测试模块的declarations数组中仅声明了宿主页面OrderDetailPage,Ionic Modal为动态创建组件,未在测试模块声明的组件无法正常完成实例初始化,需要将EditPositionModalComponent加入declarations数组,同时补充该组件依赖的所有服务Mock。
2. 直接通过Modal实例属性获取内部组件
DOM中出现两个重复的app-edit-position-modal节点是Ionic动画过渡的预渲染占位节点,没有挂载实际组件实例,不需要通过DOM查询获取组件。await modal.present()执行完成后,等待Angular变更检测队列清空,可直接通过Modal实例自带的componentInstance属性拿到内部组件的有效引用,验证传参正确性的测试代码示例:
it('should send correct data to modal', async () => { const modal: HTMLIonModalElement = await modalControllerSpy.calls.first().returnValue; await modal.present(); // 等待所有异步任务、变更检测执行完成 await fixture.whenStable(); fixture.detectChanges(); const modalComp = modal.componentInstance as EditPositionModalComponent; // 断言传入的初始数据正确 expect(modalComp.orderPosition).toEqual(testOrderPosition); });
3. 验证数据回传逻辑
不需要在测试中模拟用户在Modal内的完整输入操作,可直接调用Modal实例的dismiss方法传入模拟的修改后数据,验证宿主页面的接收处理逻辑是否正常,代码示例:
it('should receive edited data after modal dismiss', async () => { const modal: HTMLIonModalElement = await modalControllerSpy.calls.first().returnValue; await modal.present(); await fixture.whenStable(); fixture.detectChanges(); const mockUpdatedPosition = { /* 模拟用户编辑后的订单位置数据 */ }; // 模拟Modal提交后关闭,传入修改后的数据 await modal.dismiss(mockUpdatedPosition); await fixture.whenStable(); fixture.detectChanges(); // 断言宿主页面正确接收并处理了回传数据,根据实际业务逻辑调整断言字段 expect(component.currentOrderPosition).toEqual(mockUpdatedPosition); });
注意事项
- 对
modalController.create的spy必须保留and.callThrough(),如果自行mock返回值需要保证返回真实的Ionic Modal实例引用,否则无法拿到componentInstance - 所有异步操作(
present、dismiss)执行后都要调用await fixture.whenStable()并触发一次detectChanges,等Angular完成动态组件创建、变更检测后再执行断言,不要在异步方法resolve后立刻读取实例属性,避免拿到未初始化的空对象
内容的提问来源于stack exchange,提问作者R4yY

