Búsqueda de sitios web

typescript - Angular - Cómo simular un componente envoltorio con Jest y NgMocks


En nuestro proyecto podemos asignar roles de usuario a múltiples elementos a lo largo de la aplicación, lo que desactiva los elementos cuando falta el rol requerido. Pero dado que para algunos botones se aplican otras condiciones, que también pueden desactivar el componente incluso si se cumple la verificación de roles, nuestro componente también lo maneja y desactiva su elemento hijo si es necesario. La razón detrás de este diseño es que podemos mostrar fácilmente información sobre herramientas si el elemento debe estar deshabilitado.

// 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>

Este enfoque funciona bien, pero es difícil de probar unitariamente. En el código anterior, si voy a escribir una prueba unitaria donde hago clic en el botón Iniciar, seleccionarlo por ID omite el elemento de rol y llama al servicio remoto incluso si no debería. Seleccionar el elemento de rol por ID no propaga el clic en el botón.

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();
});

Estamos usando JEST junto con Spectator y NgMocks y esperaba aprovechar esa funcionalidad y simular este componente, pero no tengo idea de cómo. Y realmente no estoy seguro hasta qué punto debería intentar simularlo, ¿debería pasar el evento de clic al hijo, deshabilitar al hijo? ¿Alguna idea o recomendación sobre cómo manejar esto?

Respuesta #1

Tu caso es complicado.

Es complicado porque:

  • el botón está deshabilitado mediante MatButton, por lo tanto no puede ser simulado
  • porque queremos probar AppRoleElement, tampoco se puede simular
  • triggerEventHandler no respeta el atributo disabled y siempre dispara clics

Por lo tanto, en la prueba necesitamos:

  • mantén AppRoleElement y MatButton tal como están
  • crear un entorno especial para los casos discapacitados y habilitados
  • hacer clic en el botón a través de nativeElement

El código a continuación solo usa ng-mocks y la detección de roles se ha simplificado.

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();
  });
});