web-dev-qa-db-fra.com

Comment changer la valeur d'une case de sélection dans le test unitaire angular2?

J'ai un composant Angular2 qui contient une boîte de sélection qui ressemble à

<select [(ngModel)]="envFilter" class="form-control" name="envSelector" (ngModelChange)="onChangeFilter($event)">
    <option *ngFor="let env of envs" [ngValue]="env">{{env}}</option>
</select>

J'essaie d'écrire un test unitaire pour l'événement ngModelChange. Ceci est ma dernière tentative infructueuse

it("should filter and show correct items", async(() => {
    fixture.detectChanges();
    fixture.whenStable().then(() => {
        el = fixture.debugElement.query(By.name("envSelector"));
        fixture.detectChanges();
        makeResponse([hist2, longhist]);
        comp.envFilter = 'env3';
        el.triggerEventHandler('change', {});
        fixture.whenStable().then(() => {
            fixture.detectChanges();
            expect(comp.displayedHistory).toEqual(longhist);
        });
    });

La partie avec laquelle je rencontre des problèmes est que la modification de la valeur du modèle sous-jacent comp.envFilter = 'env3'; Ne déclenche pas la méthode de changement. J'ai ajouté el.triggerEventHandler('change', {}); mais cela jette Failed: Uncaught (in promise): ReferenceError: By is not defined. Je ne trouve aucun indice dans la documentation ... des idées?

18
Paul Becotte

En ce qui concerne l'erreur. Il semble que vous ayez juste besoin d'importer By. Ce n'est pas quelque chose qui est mondial. Il doit être importé du module suivant

import { By } from '@angular/platform-browser';

En ce qui concerne la partie test, c'est ce que j'ai pu comprendre. Lorsque vous modifiez une valeur dans un composant, vous devez déclencher une détection de modification pour mettre à jour la vue. Pour ce faire, avec fixture.detectChanges(). Une fois cela fait, normalement la vue doit être mise à jour avec la valeur.

En testant quelque chose de similaire à votre exemple, il semble que ce ne soit pas le cas. Il semble qu'il y ait encore une tâche asynchrone en cours après la détection des modifications. Disons que nous avons ce qui suit

const comp = fixture.componentInstance;
const select = fixture.debugElement.query(By.css('select'));

comp.selectedValue = 'a value;
fixture.DetectChanges();
expect(select.nativeElement.value).toEqual('1: a value');

Cela ne semble pas fonctionner. Il semble qu'il y ait une certaine asynchronisation qui empêche la valeur d'être encore définie. Nous devons donc attendre les tâches asynchrones en appelant fixture.whenStable

comp.selectedValue = 'a value;
fixture.DetectChanges();
fixture.whenStable().then(() => {
  expect(select.nativeElement.value).toEqual('1: a value');
});

Ce qui précède fonctionnerait. Mais maintenant, nous devons déclencher l'événement de changement car cela ne se produit pas automatiquement.

fixture.whenStable().then(() => {
  expect(select.nativeElement.value).toEqual('1: a value');

  dispatchEvent(select.nativeElement, 'change');
  fixture.detectChanges();
  fixture.whenStable().then(() => {
    // component expectations here
  });
});

Nous avons maintenant une autre tâche asynchrone de l'événement. Nous devons donc le stabiliser à nouveau

Voici un test complet avec lequel j'ai testé. C'est un refactor de l'exemple des tests d'intégration de code source . Ils ont utilisé fakeAsync et tick, ce qui est similaire à l'utilisation de async et whenStable. Mais avec fakeAsync, vous ne pouvez pas utiliser templateUrl, donc j'ai pensé qu'il serait préférable de le refactoriser pour utiliser async.

De plus, les tests de code source font en quelque sorte un double test unidirectionnel, testant d'abord le modèle à visualiser, puis la vue à modéliser. Bien qu'il semble que votre test essayait de faire une sorte de test bidirectionnel, d'un modèle à l'autre. Je l'ai donc un peu refactorisé pour mieux suivre votre exemple.

import { Component } from '@angular/core';
import { TestBed, getTestBed, async } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { dispatchEvent } from '@angular/platform-browser/testing/browser_util';

@Component({
  selector: 'ng-model-select-form',
  template: `
    <select [(ngModel)]="selectedCity" (ngModelChange)="onSelected($event)">
      <option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
    </select>
  `
})
class NgModelSelectForm {
  selectedCity: {[k: string]: string} = {};
  cities: any[] = [];

  onSelected(value) {
  }
}

describe('component: NgModelSelectForm', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ FormsModule ],
      declarations: [ NgModelSelectForm ]
    });
  });

  it('should go from model to change event', async(() => {
    const fixture = TestBed.createComponent(NgModelSelectForm);
    const comp = fixture.componentInstance;
    spyOn(comp, 'onSelected');
    comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
    comp.selectedCity = comp.cities[1];
    fixture.detectChanges();
    const select = fixture.debugElement.query(By.css('select'));

    fixture.whenStable().then(() => {
      dispatchEvent(select.nativeElement, 'change');
      fixture.detectChanges();
      fixture.whenStable().then(() => {
        expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
        console.log('after expect NYC');
      });
    });
  }));
});
22
Paul Samsotha

Regardez cet exemple, depuis angular (template_integration_spec.ts)

@Component({
  selector: 'ng-model-select-form',
  template: `
    <select [(ngModel)]="selectedCity">
      <option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
    </select>
  `
})
class NgModelSelectForm {
  selectedCity: {[k: string]: string} = {};
  cities: any[] = [];
}



  it('with option values that are objects', fakeAsync(() => {
       const fixture = TestBed.createComponent(NgModelSelectForm);
       const comp = fixture.componentInstance;
       comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
       comp.selectedCity = comp.cities[1];
       fixture.detectChanges();
       tick();

       const select = fixture.debugElement.query(By.css('select'));
       const nycOption = fixture.debugElement.queryAll(By.css('option'))[1];

       // model -> view
       expect(select.nativeElement.value).toEqual('1: Object');
       expect(nycOption.nativeElement.selected).toBe(true);

       select.nativeElement.value = '2: Object';
       dispatchEvent(select.nativeElement, 'change');
       fixture.detectChanges();
       tick();

       // view -> model
       expect(comp.selectedCity['name']).toEqual('Buffalo');
     }));
7

J'ai trouvé la réponse de peeskillet très utile, mais malheureusement, elle est un peu dépassée car la façon d'envoyer un événement a été modifiée. J'ai également constaté qu'il y avait un appel inutile à whenStable (). Voici donc un test mis à jour en utilisant la configuration de peeskillet:

    it('should go from model to change event', async(() => {
        const fixture = TestBed.createComponent(NgModelSelectForm);
        const comp = fixture.componentInstance;
        spyOn(comp, 'onSelected');
        comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
        comp.selectedCity = comp.cities[1];
        fixture.detectChanges();
        const select = fixture.debugElement.query(By.css('select'));

        fixture.whenStable().then(() => {
            select.nativeElement.dispatchEvent(new Event('change'));
            fixture.detectChanges();
            expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
            console.log('after expect NYC');
        });
    }));
6
Iain

J'espère que cela aidera quelqu'un. Même problème que celui soulevé par OP mais code légèrement différent.

Fonctionne en Angular 7.

HTML:

<select id="dashboard-filter" class="form-control" name="dashboard-filter" [ngModel]="dashboardFilterValue" (ngModelChange)="onFilterChange($event)"
              [disabled]="disabled">
  <option *ngFor="let filter of dashboardFilters" [ngValue]="filter.value">{{ filter.name }}</option>
</select>

Test de l'unité:

it('onFilterChange', () => {

  // ensure dropdown is enabled
  expect(component.disabled).toBe(false)

  // spies
  spyOn(component, 'onFilterChange').and.callThrough()
  spyOn(component.filterChange, 'emit')

  // initially the 3rd item in the dropdown is selected
  const INITIAL_FILTER_INDEX = 2
  // we want to select the 5th item in the dropdown
  const FILTER_INDEX = 4
  // the expected filter value is the value of the 5th dashboard filter (as used to populate the dropdown)
  const EXPECTED_FILTER_VALUE = getDashboardFiltersData.dashboardFilters[FILTER_INDEX].value

  // handle on the dropdown
  const filterDropdown = fixture.debugElement.query(By.css('select')).nativeElement

  // let bindings complete
  fixture.whenStable().then(() => {

    // ensure filterDropdown.value is stable
    expect(filterDropdown.value).toContain(getDashboardFiltersData.dashboardFilters[INITIAL_FILTER_INDEX].value)

    // update filterDropdown.value and dispatch change event
    filterDropdown.value = filterDropdown.options[FILTER_INDEX].value
    filterDropdown.dispatchEvent(new Event('change'))

    // check component data
    expect(component.dashboardFilterValue).toBe(EXPECTED_FILTER_VALUE)
    expect(component.dashboardFilterChangeInProgress).toBe(false)

    // check spies
    expect(component.onFilterChange).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
    expect(setDashboardFilterSpy).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
    expect(component.filterChange.emit).toHaveBeenCalledWith(true)
  })
})
0
danday74