在Angular 5.2.0项目中,我具有以下结构:
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private _title = 'initial value';
public get title(): string {
return this._title;
}
public set title(v: string) {
this._title = v;
}
}
app.component.spec.ts
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
imports: [FormsModule]
}).compileComponents();
}));
it('should bind an input to a property', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
fixture.detectChanges();
// Update the title input
const inputElement = fixture.debugElement.query(By.css('input[name="title"]')).nativeElement;
inputElement.value = 'new value';
inputElement.dispatchEvent(new Event('input'));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(app.title).toEqual('new value');
});
}));
});
并通过以下测试:
app.component.html
<input name="title" type="text" [(ngModel)]="title">
但是,如果我将输入放入表单标签,测试将失败:
app.component.html
<form>
<input name="title" type="text" [(ngModel)]="title">
</form>
Chrome 67.0.3396(Windows 7 0.0.0)AppComponent应该将输入绑定到属性FAILED 预期“初始值”等于“新值”。
有什么想法为什么会发生以及如何解决?
答案 0 :(得分:1)
第一个解决方案(使用fakeAsync + tick):
it('should bind an input to a property', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
fixture.detectChanges();
tick();
const inputElement = fixture.debugElement.query(By.css('input[name="title"]')).nativeElement;
inputElement.value = 'new value';
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
tick();
expect(app.title).toEqual('new value');
}));
第二个解决方案(使用同步和少量代码重构):
describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
let app: AppComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({...}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
app = fixture.debugElement.componentInstance;
fixture.detectChanges(); // this call is required
}));
it('should bind an input to a property', async(() => {
const inputElement = fixture.debugElement.query(By.css('input[name="title"]')).nativeElement;
inputElement.value = 'new value';
inputElement.dispatchEvent(new Event('input'));
fixture.whenStable().then(() => {
expect(app.title).toEqual('new value');
});
}));
...
知道为什么会发生吗?
模板驱动的窗体将其窗体控件的创建委托给指令。为了避免检查错误后进行更改,这些指令要花费一个以上的周期来构建整个控制树。这意味着您必须等到下一个更改检测周期发生之后,才能从组件类中操作任何控件。
例如,如果您使用@ViewChild(NgForm)查询注入表单控件,并在ngAfterViewInit生命周期挂钩中对其进行检查,则会发现它没有子对象。您必须使用setTimeout()触发更改检测周期,然后才能从控件中提取值,测试其有效性或将其设置为新值。
p.s。此外,Angular's GitHub repo中也存在类似的问题(dispatchEvent不会触发ngModel更改#13550),您也可以查看一下。