I wanted to use template forms and [min]
and [max]
directives, so I have created them and they work. But the test confuses me: validation is not executed asynchronously, yet after changing my values and stuff, I have to go through this:
component.makeSomeChangeThatInvalidatesMyInput();
// control.invalid = false, expected
fixture.detectChanges();
// control.invalid is still false, not expected
// but if I go do this
fixture.whenStable().then(() => {
// control.invalid is STILL false, not expected
fixture.detectChanges();
// control.invalid now true
// expect(... .errors ... ) now passes
})
I don't understand why would I need even that whenStable()
, let alone another detectChanges()
cycle. What am I missing here? Why do I need 2 cycles of change detection for this validation to be executed?
Doesn't matter if I run the test as async
or not.
Here's my test:
@Component({
selector: 'test-cmp',
template: `<form>
<input [max]="maxValue" [(ngModel)]="numValue" name="numValue" #val="ngModel">
<span class="error" *ngIf="val.invalid">Errors there.</span>
</form>`
})
class TestMaxDirectiveComponent {
maxValue: number;
numValue: number;
}
fdescribe('ValidateMaxDirective', () => {
let fixture: ComponentFixture<TestMaxDirectiveComponent>;
let component: TestMaxDirectiveComponent;
beforeEach(async(() => TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestMaxDirectiveComponent, ValidateMaxDirective],
}).compileComponents()
.then(() => {
fixture = TestBed.createComponent(TestMaxDirectiveComponent);
component = fixture.componentInstance;
return fixture.detectChanges();
})
));
fit('should have errors even when value is greater than maxValue', async(() => {
component.numValue = 42;
component.maxValue = 2;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.error')).toBeTruthy();
});
}));
});
And here's the directive itself (simplified a bit):
const VALIDATE_MAX_PROVIDER = {
provide: NG_VALIDATORS, useExisting: forwardRef(() => ValidateMaxDirective), multi: true,
};
@Directive({
selector: '[max][ngModel]',
providers: [VALIDATE_MAX_PROVIDER],
})
export class ValidateMaxDirective implements Validator {
private _max: number | string;
@Input() get max(): number | string {
return this._max;
}
set max(value: number | string) {
this._max = value;
}
validate(control: AbstractControl): ValidationErrors | null {
if (isEmptyInputValue(control.value) || isEmptyInputValue(this._max)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
return !isNaN(value) && value > this._max ? {'max': {'max': this._max, 'actual': control.value}} : null;
}
}
I have tested this on a brand new ng new app
with @angular/cli
version 1.6.8 and latest angular 5.2.
whenStable
before as you do, but tests are very complex to get them right for non simple test case like testing many condition one after another. I prefer to create non async tests using fakeAsync and/or tick. this way you have more control on your tests. you can try to create a demo like I did here in one of my answers that you can let people to understand and help you more: stackblitz.com/edit/angular-testing-c25ezq?file=app/… – Stilt.detectChanges();
without importingChangeDetectorRef
? – TrottierChangeDetectionStrategy
onComponentFixture
the way you can on aChangeDetectorRef
or if you can, that is what you should do : angular.io/api/core/ChangeDetectionStrategy#OnPush if I follow correctly, you wantChangeDetectionStrategy.Default
– TrottierdetectChanges()
inside a.whenStable()
's callback and successfully get the current state. come to think of it this is how all subscribables (including observables) work. – Trottier