如何使用Jasmine为Angular Material cdkDragEntered编写单元测试
问题背景
基于Angular和Angular Material CDK实现的拖拽排序组件功能正常,但无法为dragEntered()方法编写单元测试,无法触发cdkDragEntered事件拿到代码覆盖率。
组件实现代码
HTML代码
<div class="sortable-list-view-container" cdkDropListGroup> <div [ngClass]="itemClass" *ngFor="let item of items; let i = index" cdkDropList [cdkDropListData]="i"> <div cdkDrag [cdkDragData]="i" (cdkDragEntered)="dragEntered($event)" class="sortable-list-view-box" > <ng-container *ngTemplateOutlet="itemTemplate; context: { $implicit: item }"></ng-container> </div> </div> </div>
TypeScript逻辑代码
import { CdkDragEnter, moveItemInArray } from '@angular/cdk/drag-drop'; import { Component, ViewEncapsulation, Input, ContentChild, TemplateRef, Output, EventEmitter, } from '@angular/core'; @Component({ selector: 'sortable-list-view', templateUrl: './sortable-list-view.component.html', styleUrls: ['./sortable-list-view.component.scss'], encapsulation: ViewEncapsulation.None }) export class SortableListViewComponent { @Input('items') public items: any[] = []; @Input('item-class') public itemClass: string; @Output('sort') public sort: EventEmitter<any> = new EventEmitter(); @ContentChild(TemplateRef) public itemTemplate: TemplateRef<any>; private phElement: HTMLElement; public dragEntered(event: CdkDragEnter<number>) { const drag = event.item; const dropList = event.container; const dragIndex = drag.data; const dropIndex = dropList.data; const phContainer = dropList.element.nativeElement; this.phElement = phContainer.querySelector('.cdk-drag-placeholder'); if( this.phElement != null){ this.phElement.style.width = (phContainer.clientWidth - 2) + 'px'; this.phElement.style.height = phContainer.clientHeight + 'px'; phContainer.removeChild(this.phElement); phContainer.parentElement.insertBefore( this.phElement, phContainer); } if(dropIndex !== dragIndex){ moveItemInArray(this.items, dragIndex, dropIndex); this.sort.emit(this.items); } } }
已尝试的失败方案
方案1:手动派发原生拖拽事件
手动派发drag/drop/mousemove等原生事件,无法触发dragEntered回调。
方案2:手动构造CdkDragEnter对象调用
强转不完整的对象为CdkDragEnter类型,要么无法统计代码覆盖率,要么callThrough时触发CDK内部死循环,导致Karma浏览器无响应卡死。
可行解决方案
Angular CDK本身的拖拽触发逻辑已经有官方测试覆盖,你不需要重复测试这部分,只需要构造合法的CdkDragEnter参数,测试你自己写的业务逻辑即可,即可拿到完整代码覆盖率,也不会出现浏览器卡死问题。
正确测试代码示例
import {ComponentFixture, fakeAsync, TestBed, flush} from '@angular/core/testing'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import { SortableListViewComponent } from './sortable-list-view.component'; import { SortableListViewModule } from './sortable-list-view.module'; import { By } from '@angular/platform-browser'; import { Component, ViewChild } from '@angular/core'; import { CdkDragEnter, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop'; @Component({ selector: 'test-component', template: ` <sortable-list-view [items]="items"> <ng-template let-item>{{item}}</ng-template> </sortable-list-view>` }) class TestSortableListViewComponent { @ViewChild(SortableListViewComponent) public sortableList: SortableListViewComponent; public items = [1, 2, 3, 4, 5]; } describe('SortableListViewComponent', () => { let component: TestSortableListViewComponent; let fixture: ComponentFixture<TestSortableListViewComponent>; beforeEach(() => { TestBed.configureTestingModule({ declarations: [TestSortableListViewComponent], imports: [NoopAnimationsModule, SortableListViewModule] }); fixture = TestBed.createComponent(TestSortableListViewComponent); component = fixture.componentInstance; fixture.detectChanges(); spyOn(component.sortableList.sort, 'emit'); }); it('should run dragEntered logic correctly and update items', fakeAsync(() => { // 从DOM节点的注入器拿到真实的CdkDrag、CdkDropList实例,避免手动构造对象缺失属性 const dragItemEl = fixture.debugElement.queryAll(By.css('.sortable-list-view-box'))[0]; const dragInstance = dragItemEl.injector.get(CdkDrag) as CdkDrag<number>; const dropContainerEl = fixture.debugElement.queryAll(By.css('[cdkDropList]'))[2]; // 模拟拖到第3个位置 const dropInstance = dropContainerEl.injector.get(CdkDropList) as CdkDropList<number>; // 模拟CDK拖拽过程中生成的占位符元素 const placeholder = document.createElement('div'); placeholder.classList.add('cdk-drag-placeholder'); dropContainerEl.nativeElement.appendChild(placeholder); fixture.detectChanges(); // 构造完整合法的CdkDragEnter事件 const dragEnterEvent: CdkDragEnter<number> = { item: dragInstance, container: dropInstance, event: new MouseEvent('dragenter') } as CdkDragEnter<number>; // 直接调用目标方法,不需要spy拦截执行,即可统计代码覆盖率 component.sortableList.dragEntered(dragEnterEvent); fixture.detectChanges(); flush(); // 验证逻辑执行结果 expect(component.sortableList.items).toEqual([2,3,1,4,5]); // 原索引0的元素移到索引2的位置 expect(component.sortableList.sort.emit).toHaveBeenCalledWith([2,3,1,4,5]); })); });
原方案问题说明
之前手动构造CdkDragEnter对象时用as强转不完整的对象,CDK内部访问缺失的属性时会触发死循环导致浏览器卡死。从节点注入器拿到真实的CDK实例构造事件,所有属性完整不会出现死循环。
如果需要模拟完整的原生拖拽流程,可参考Angular CDK官方仓库中drag-and-drop.spec.ts的测试工具函数,模拟鼠标事件时移动距离要超过CDK默认的5px拖拽触发阈值,即可触发cdkDragEntered回调。
内容的提问来源于stack exchange,提问作者Ayman Safi
相关产品推荐
相关产品推荐

