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

Angular指令手动正常但Karma Jasmine单元测试失败求助

PopupTargetDirective单元测试失败问题排查

问题描述

我正在为一款点击时触发弹窗的PopupTargetDirective编写单元测试。该指令手动测试完全正常,但Karma Jasmine单元测试却显示失败,我对此无法理解。

测试代码

@Component({
  template: '<button class="btn" PopupTarget target="my_modal">open modal</button>' +
    '<popup popupId="my_modal">hello</popup>'
})
export class TesterComponent {
}


describe('PopupTargetDirective', () => {

  let fixture: ComponentFixture<TesterComponent>
  let directive: PopupTargetDirective;
  let button: HTMLButtonElement;
  let popup: DebugElement

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [PopupTargetDirective, TesterComponent, PopupComponent]
    })
      .compileComponents();

    fixture = TestBed.createComponent(TesterComponent);
    button = fixture.debugElement.nativeElement.querySelector(".btn")
    directive = new PopupTargetDirective(new ElementRef<any>(button));
    popup = fixture.debugElement.query(By.directive(PopupComponent)).nativeElement
  });

  it('should create an instance', () => {
    expect(directive).toBeTruthy();
  })

  it('should show popup when clicked', () => {
    let dialog: HTMLDialogElement | null = fixture.debugElement.nativeElement.querySelector("dialog")
    console.log(button)

    expect(dialog?.open).toBeFalse()
    button.click()
    fixture.whenStable().then(() => {
      expect(dialog?.open).toBeTrue()
    })
  })
});

指令代码

@Directive({
  selector: '[PopupTarget]'
})
export class PopupTargetDirective {

  @Input() target : string = '';

  constructor(private el: ElementRef) {
    el.nativeElement.addEventListener("click", ()=>{
      let targetPopup: HTMLDialogElement | null = document.getElementById(this.target) as HTMLDialogElement;
      targetPopup?.showModal()
    })
  }
}

弹窗HTML

<dialog #dialog id="{{popupId}}" class="modal tw-rounded-md"
        (close)="this.onClose.emit()"
        (cancel)="cancel($event)"
>

  <form method="dialog" class="modal-box">
    <div class="tw-flex tw-flex-col tw-justify-center">
      <button #closeButton class="tw-self-end" *ngIf="showCloseButton">✕</button>
      <ng-content class="tw-self-auto"></ng-content>
    </div>
  </form>
</dialog>

弹窗TS代码

@Component({
  selector: 'popup',
  templateUrl: './popup.component.html'
})
export class PopupComponent {

  @Input() showCloseButton: boolean = false;
  @Input({required: true}) popupId: string | undefined;
  @Output() onClose: EventEmitter<void> = new EventEmitter()
  @ViewChild("dialog") dialog: ElementRef<HTMLDialogElement> | undefined
  @ViewChild("closeButton") closeButton: ElementRef<HTMLButtonElement> | undefined

  cancel(event: Event) {
    if (!this.showCloseButton) event.preventDefault()
    else
      this.onClose.emit()
  }
}

请问我的测试代码是否存在问题?如前所述,该指令手动测试完全正常,问题大概率出在测试代码上。


测试代码的问题分析

你的测试代码存在三个核心问题:

  • 手动实例化指令导致输入属性未绑定
    你通过new PopupTargetDirective(new ElementRef<any>(button))手动创建指令实例,但Angular并未参与这个过程,因此@Input() target属性不会被赋值为模板中的"my_modal"。点击按钮时,指令里的this.target是空字符串,自然找不到对应的弹窗元素。

  • 未触发变更检测初始化视图
    创建组件后没有调用fixture.detectChanges(),Angular不会渲染模板、绑定输入属性,也不会初始化指令和组件的生命周期钩子,导致弹窗的id没有被正确渲染,指令也没有被正确初始化。

  • 异步测试未正确处理
    测试中使用fixture.whenStable().then()但没有告知Jasmine等待异步操作完成,测试会直接跳过断言部分就结束,导致断言永远不会执行或执行时测试已经结束。


修正后的测试代码

@Component({
  template: '<button class="btn" PopupTarget target="my_modal">open modal</button>' +
    '<popup popupId="my_modal">hello</popup>'
})
export class TesterComponent {
}


describe('PopupTargetDirective', () => {

  let fixture: ComponentFixture<TesterComponent>;
  let directive: PopupTargetDirective;
  let buttonDebugEl: DebugElement;
  let button: HTMLButtonElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [PopupTargetDirective, TesterComponent, PopupComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(TesterComponent);
    // 获取按钮的DebugElement,用于获取指令实例
    buttonDebugEl = fixture.debugElement.query(By.css(".btn"));
    button = buttonDebugEl.nativeElement;
    // 从DebugElement中获取Angular管理的指令实例,而非手动创建
    directive = buttonDebugEl.injector.get(PopupTargetDirective);
    // 触发变更检测,初始化视图和绑定
    fixture.detectChanges();
  });

  it('should create an instance', () => {
    expect(directive).toBeTruthy();
  });

  // 使用async/await处理异步操作
  it('should show popup when clicked', async () => {
    const dialog: HTMLDialogElement | null = fixture.debugElement.nativeElement.querySelector("dialog");
    
    // 初始状态弹窗未打开
    expect(dialog?.open).toBeFalse();
    
    button.click();
    // 等待异步操作稳定
    await fixture.whenStable();
    // 再次触发变更检测,更新视图状态
    fixture.detectChanges();
    
    expect(dialog?.open).toBeTrue();
  });
});

内容的提问来源于stack exchange,提问作者Heila Al-Mogren

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 09:15:02