Tìm kiếm trang web

typescript - Angular - Cách mô phỏng component bao bọc với Jest và NgMocks


Trong dự án của chúng tôi, chúng tôi có thể gán vai trò người dùng cho nhiều phần tử trong ứng dụng, điều này sẽ vô hiệu hóa các phần tử khi vai trò cần thiết bị thiếu. Nhưng vì đối với một số nút, các điều kiện khác cũng được áp dụng, có thể vô hiệu hóa thành phần ngay cả khi kiểm tra vai trò thành công, nên thành phần của chúng tôi cũng sẽ tự xử lý và vô hiệu hóa các phần tử con nếu cần thiết. Lý do đằng sau thiết kế này là để chúng tôi có thể dễ dàng hiển thị tooltip nếu phần tử cần bị vô hiệu hóa.

// app-role-element
<div matTooltip="{{ tooltipMessage | translate }}" [matTooltipDisabled]="!isDisabled">
  <ng-content> </ng-content>
</div>
// .ts
export class AppRoleElement implements AfterConentInit {
  @Input('roles') roles: string[];
  @ContentChild('roleElement') roleElement: MatButton;

  constructor(...){}    

  ngAfterContentInit(): void {
    ...
    this.setDisabled();
  }

  setDisabled(): void {
    if (this.roleElement) {
      if (this.isDisabled) {
        this.roleElement.disabled = true; // disable if no role
      } else {
        this.roleElement.disabled = this.disableCondition; // disable if other condition
      }
    }
  }
}

// usage
<app-role-component
  [role]="['required-role']"
  [disableCondition]= "isRunning || isUnstable"
  [id]="startButtonRoleElem"
>
  <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
</app-role-component>

Cách tiếp cận này hoạt động tốt nhưng khó để kiểm thử đơn vị. Trong đoạn mã ở trên, nếu tôi viết một bài kiểm thử đơn vị mà tôi nhấn vào nút Bắt đầu, việc chọn nó theo ID sẽ bỏ qua phần tử theo vai trò và gọi dịch vụ từ xa ngay cả khi không nên. Việc chọn phần tử theo vai trò bằng ID sẽ không truyền sự kiện nhấn trên nút.

test('to prevent click on a start btn when form is invalid', () => {
  spectator.component.runEnabled$ = of(false);
  spectator.detectComponentChanges();

  const checkExportFolderSpy = jest.spyOn(spectator.component, 'checkExportFolder');
  spectator.inject(PreferencesService).validateCurrentExportPath.andReturn(of(VALIDATION_RESULT_OK));
  spectator.detectChanges();

  spectator.click('#startBtnId');
  spectator.detectChanges();

  expect(checkExportFolderSpy).not.toHaveBeenCalled();
  expect(dispatchSpy).not.toHaveBeenCalled();
});

Chúng tôi đang sử dụng JEST cùng với Spectator và NgMocks và tôi hy vọng có thể tận dụng chức năng đó để giả lập (mock) component này, nhưng tôi không biết phải làm thế nào. Và tôi cũng không chắc nên thử giả lập nó đến mức nào, có nên truyền sự kiện click cho child, tắt child không? Có ai có ý tưởng hoặc đề xuất nào về cách xử lý vấn đề này không?

Câu trả lời #1

Trường hợp của bạn là một trường hợp phức tạp.

Nó phức tạp vì:

  • nút bị vô hiệu hóa thông qua MatButton, do đó nó không thể bị giả lập
  • bởi vì chúng tôi muốn kiểm tra AppRoleElement, nó cũng không thể bị giả lập
  • triggerEventHandler không tôn trọng thuộc tính disabled và luôn kích hoạt các lần nhấp

Vì vậy trong bài kiểm tra chúng ta cần:

  • giữ AppRoleElement và MatButton như hiện tại
  • tạo một môi trường đặc biệt cho các trường hợp người khuyết tật và người không khuyết tật
  • nhấn nút thông qua nativeElement

Mã dưới đây chỉ sử dụng ng-mocks và việc phát hiện vai trò đã được đơn giản hóa.

import {AfterContentInit, Component, ContentChild, Input, NgModule,} from '@angular/core';
import {MatButton, MatButtonModule} from '@angular/material/button';
import {MockBuilder, MockRender, ngMocks} from 'ng-mocks';

@Component({
  selector: 'app-role-component',
  template: `
    <div>
      <ng-content></ng-content>
    </div>
  `,
})
class AppRoleElement implements AfterContentInit {
  @Input() public disable: boolean | null = null;
  @ContentChild(MatButton) public roleElement?: MatButton;

  public ngAfterContentInit(): void {
    this.setDisabled();
  }

  public setDisabled(): void {
    if (this.roleElement) {
      this.roleElement.disabled = this.disable;
    }
  }
}

@NgModule({
  declarations: [AppRoleElement],
  imports: [MatButtonModule],
})
class AppModule {}

fdescribe('ng-mocks-click', () => {
  // Keeping AppRoleElement and MatButton
  beforeEach(() => MockBuilder(AppRoleElement, AppModule).keep(MatButton));

  it('is not able to click the disabled button', () => {
    // specific params for the render
    const params = {
      disabled: true,
      start: jasmine.createSpy('start'),
    };

    // rendering custom template
    MockRender(
      `
      <app-role-component
        [disable]="disabled"
      >
        <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
      </app-role-component>
    `,
      params,
    );

    // click on a disable element isn't propagandized
    ngMocks.find('button').nativeElement.click();

    // asserting
    expect(params.start).not.toHaveBeenCalled();
  });

  // checking the enabled case
  it('is able to click the button', () => {
    const params = {
      disabled: false,
      start: jasmine.createSpy('start'),
    };

    MockRender(
      `
      <app-role-component
        [disable]="disabled"
      >
        <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
      </app-role-component>
    `,
      params,
    );

    ngMocks.find('button').nativeElement.click();
    expect(params.start).toHaveBeenCalled();
  });
});