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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 12:39:01