Первый пример
Я получил следующий тест:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
@Component({
template: '<ul><li *ngFor="let state of values | async">{{state}}</li></ul>'
})
export class TestComponent {
values: Promise<string[]>;
}
describe('TestComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
element = (<HTMLElement>fixture.nativeElement);
});
it('this test fails', async() => {
// execution
component.values = Promise.resolve(['A', 'B']);
fixture.detectChanges();
await fixture.whenStable();
// evaluation
expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
});
it('this test works', async() => {
// execution
component.values = Promise.resolve(['A', 'B']);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
await fixture.whenStable();
// evaluation
expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
});
});
Как вы можете видеть, есть супер простой компонент, который просто отображает список элементов, предоставленных Promise
. Есть два теста, один из которых не пройден, а другой пройден. Единственное различие между этими тестами состоит в том, что тест, который прошел, вызывает fixture.detectChanges(); await fixture.whenStable();
fixture.detectChanges(); await fixture.whenStable();
дважды.
ОБНОВЛЕНИЕ: Второй пример (обновлено снова 2019/03/21)
Этот пример пытается исследовать возможные отношения с ngZone:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { Component, NgZone } from '@angular/core';
@Component({
template: '{{value}}'
})
export class TestComponent {
valuePromise: Promise<ReadonlyArray<string>>;
value: string = '-';
set valueIndex(id: number) {
this.valuePromise.then(x => x).then(x => x).then(states => {
this.value = states[id];
console.log('value set ${this.value}. In angular zone? ${NgZone.isInAngularZone()}');
});
}
}
describe('TestComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestComponent],
providers: [
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
function diagnoseState(msg) {
console.log('Content: ${(fixture.nativeElement as HTMLElement).textContent}, value: ${component.value}, isStable: ${fixture.isStable()} # ${msg}');
}
it('using ngZone', async() => {
// setup
diagnoseState('Before test');
fixture.ngZone.run(() => {
component.valuePromise = Promise.resolve(['a', 'b']);
// execution
component.valueIndex = 1;
});
diagnoseState('After ngZone.run()');
await fixture.whenStable();
diagnoseState('After first whenStable()');
fixture.detectChanges();
diagnoseState('After first detectChanges()');
});
it('not using ngZone', async(async() => {
// setup
diagnoseState('Before setup');
component.valuePromise = Promise.resolve(['a', 'b']);
// execution
component.valueIndex = 1;
await fixture.whenStable();
diagnoseState('After first whenStable()');
fixture.detectChanges();
diagnoseState('After first detectChanges()');
await fixture.whenStable();
diagnoseState('After second whenStable()');
fixture.detectChanges();
diagnoseState('After second detectChanges()');
await fixture.whenStable();
diagnoseState('After third whenStable()');
fixture.detectChanges();
diagnoseState('After third detectChanges()');
}));
});
Этот первый из этих тестов (явно использующий ngZone) приводит к:
Content: -, value: -, isStable: true # Before test
Content: -, value: -, isStable: false # After ngZone.run()
value set b. In angular zone? true
Content: -, value: b, isStable: true # After first whenStable()
Content: b, value: b, isStable: true # After first detectChanges()
Журналы второго теста:
Content: -, value: -, isStable: true # Before setup
Content: -, value: -, isStable: true # After first whenStable()
Content: -, value: -, isStable: true # After first detectChanges()
Content: -, value: -, isStable: true # After second whenStable()
Content: -, value: -, isStable: true # After second detectChanges()
value set b. In angular zone? false
Content: -, value: b, isStable: true # After third whenStable()
Content: b, value: b, isStable: true # After third detectChanges()
Я как бы ожидал, что тест проходит в угловой зоне, но это не так. Кажется, проблема в том, что
Чтобы избежать неожиданностей, функции, передаваемые в then(), никогда не будут вызываться синхронно, даже с уже решенным обещанием. (Источник)
Во втором примере я спровоцировал проблему, несколько раз вызывая .then(x => x)
, что не более чем снова помещает прогресс в цикл событий браузера и, таким образом, задерживает результат. Насколько я понимаю, вызов await fixture.whenStable()
должен в основном сказать: "подождите, пока эта очередь не станет пустой". Как мы видим, это действительно работает, если я выполняю код в ngZone явно. Однако это не по умолчанию, и я не могу найти где-нибудь в руководстве, что предполагается, что я пишу свои тесты таким образом, так что это чувствует себя неловко.
Что на самом деле await fixture.whenStable()
во втором тесте? Исходный код показывает, что в этом случае fixture.whenStable()
просто return Promise.resolve(false);
, Поэтому я попытался заменить await fixture.whenStable()
на await Promise.resolve()
и это действительно имеет тот же эффект: это приводит к приостановке теста и его valuePromise.then(...)
очереди событий, и, таким образом, обратный вызов передается в valuePromise.then(...)
на самом деле выполняется, если я просто достаточно часто называю await
любого обещания.
Почему мне нужно вызвать await fixture.whenStable();
многократно? Я использую это неправильно? Это намеренное поведение? Есть ли "официальная" документация о том, как она предназначена для работы/как с этим бороться?