NgModel 绑定在 Jasmine 测试的 <form> 中不起作用

NgModel binding doesn't work inside <form> for Jasmine test

在我的 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 预期 'initial value' 等于 'new value'.

知道为什么会发生这种情况以及如何解决这个问题吗?

第一个解决方案(使用 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');
    });
  }));
  ...

Any idea why it is happening?

根据official Angular docs

Template-driven forms delegate the creation of their form controls to directives. To avoid changed after checked errors, these directives take more than one cycle to build the entire control tree. That means you must wait until the next change detection cycle happens before manipulating any of the controls from within the component class.

For example, if you inject the form control with a @ViewChild(NgForm) query and examine it in the ngAfterViewInit lifecycle hook, you'll discover that it has no children. You must trigger a change detection cycle using setTimeout() before you can extract a value from a control, test its validity, or set it to a new value.

p.s。 Angular's GitHub repo也有类似的问题(dispatchEvent doesn't trigger ngModel changes #13550),你也可以看看