Angular2 NgModel not getting value in Jasmine test

爷,独闯天下 提交于 2019-12-03 12:23:57

问题


I am using template-driven forms in Angular 2, and I'm trying to develop them test-first. I've scoured this site and the rest of the internet and I've tried basically everything I can find (mainly bunches of tick() statements and detectChanges() everywhere in a fakeAsync) to get the NgModel attached to my input to pick up the value so it can be passed to my onSubmit function. The value of the input element sets properly, but the NgModel never updates, which then means the onSubmit function does not get the correct value from the NgModel.

Here's the template:

<form id="createWorkout" #cwf="ngForm" (ngSubmit)="showWorkout(skillCountFld)" novalidate>
  <input name="skillCount" id="skillCount" class="form-control" #skillCountFld="ngModel" ngModel />
  <button type="submit" id="buildWorkout">Build a Workout</button>
</form>

Note: I know that the value sent the ngSubmit is going to cause the test to fail, but it means I can set a break point in the function and inspect the NgModel.

Here's the Component:

import { Component, OnInit } from '@angular/core';
import {SkillService} from "../model/skill-service";
import {NgModel} from "@angular/forms";

@Component({
  selector: 'app-startworkout',
  templateUrl: './startworkout.component.html',
  styleUrls: ['./startworkout.component.css']
})
export class StartworkoutComponent implements OnInit {
  public skillCount:String;

  constructor(public skillService:SkillService) { }

  showWorkout(value:NgModel):void {
    console.log('breakpoint', value.value);
  }

  ngOnInit() {
  }

}

Here is the spec:

/* tslint:disable:no-unused-variable */
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {By, BrowserModule} from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { StartworkoutComponent } from './startworkout.component';
import {SkillService} from "../model/skill-service";
import {Store} from "../core/store";
import {SportService} from "../model/sport-service";
import {FormsModule} from "@angular/forms";
import {dispatchEvent} from "@angular/platform-browser/testing/browser_util";

describe('StartworkoutComponent', () => {
  let component: StartworkoutComponent;
  let fixture: ComponentFixture;
  let element:DebugElement;
  let skillService:SkillService;

  beforeEach(async(() => {
    var storeSpy:any = jasmine.createSpyObj('store', ['getValue', 'storeValue', 'removeValue']);
    var stubSkillService:SkillService = new SkillService(storeSpy);
    TestBed.configureTestingModule({
      declarations: [ StartworkoutComponent ],
      providers: [{provide:Store , useValue:storeSpy}, SportService, SkillService],
      imports: [BrowserModule, FormsModule]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(StartworkoutComponent);
    component = fixture.componentInstance;
    element = fixture.debugElement;
    fixture.detectChanges();

  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  describe('without workout', () => {
    let createWorkout:DebugElement;
    let skillCount:HTMLInputElement;
    let submitButton:HTMLButtonElement;
    beforeEach(() => {
      createWorkout = element.query(By.css('#createWorkout'));
      skillCount = element.query(By.css('#skillCount')).nativeElement;
      submitButton = element.query(By.css('#buildWorkout')).nativeElement;
    });
    it('has createWorkout form', () => {
      expect(createWorkout).toBeTruthy();
      expect(skillCount).toBeTruthy();
    });
    it('submits the value', fakeAsync(() => {
      spyOn(component, 'showWorkout').and.callThrough();
      tick();
      skillCount.value = '10';
      dispatchEvent(skillCount, 'input');
      fixture.detectChanges();
      tick(50);
      submitButton.click();
      fixture.detectChanges();
      tick(50);
      expect(component.showWorkout).toHaveBeenCalledWith('10');
    }));
  });
});

I'm sure I'm missing something basic/simple, but I've spent the past day combing through everything I can find with no luck.

Edit:

I think maybe people are focusing on the wrong thing. I'm pretty sure at this point that I'm missing something basic about how ngForm and ngModel work. When I add

&lt;p>{{cwf.value | json}}&lt;/p>

into the form, it just shows {}. I believe it should show a member property representing the input. If I type into the field, the value does not change. Similar things happen if I try to bind to skillCountFld. So I think the basic form setup is incorrect somehow, and the test is never going to work until the input is correctly wired to the skillCountFld controller. I just don't see what I'm missing.


回答1:


There are a lot of tests at the Angular site that are successfully setting this without waiting for whenStable https://github.com/angular/angular/blob/874243279d5fd2bef567a13e0cef8d0cdf68eec1/modules/%40angular/forms/test/template_integration_spec.ts#L1043

That's because all code in those tests is executed inside fakeAsync zone while you are firing fixture.detectChanges(); within beforeEach. So fakeAsync zone doesn't know about async operation outside its scope. When you're calling detectChanges first time ngModel is initialized

 NgModel.prototype.ngOnChanges = function (changes) {
            this._checkForErrors();
            if (!this._registered)
                this._setUpControl(); //<== here

and gets right callback for input event

NgForm.prototype.addControl = function (dir) {
  var _this = this;
  resolvedPromise.then(function () { // notice async operation
      var container = _this._findContainer(dir.path);
      dir._control = (container.registerControl(dir.name, dir.control));
      setUpControl(dir.control, dir); // <== here

inside setUpControl you can see function that will be called by input event

dir.valueAccessor.registerOnChange(function (newValue) {
  dir.viewToModelUpdate(newValue);
  control.markAsDirty();
  control.setValue(newValue, { emitModelToViewChange: false });
});

1) So if you move fixture.detectChanges from beforeEach to your test then it should work:

 it('submits the value', fakeAsync(() => {
   spyOn(component, 'showWorkout').and.callThrough();
   fixture.detectChanges();

   skillCount = element.query(By.css('#skillCount')).nativeElement;
   submitButton = element.query(By.css('#buildWorkout')).nativeElement;

   tick();
   skillCount.value = '10';
   dispatchEvent(skillCount, 'input');
   fixture.detectChanges();

   submitButton.click();
   fixture.detectChanges();
   expect(component.showWorkout).toHaveBeenCalledWith('10');
}));

Plunker Example

But this solution seems very complicated since you need to rewrite your code to move fixture.detectChanges in each of your it statements (and there is also a problem with skillCount, submitButton etc)

2) As Dinistro said async together with whenStable should also help you:

it('submits the value', async(() => {
  spyOn(component, 'showWorkout').and.callThrough();
  fixture.whenStable().then(() => {
    skillCount.value = '10';
    dispatchEvent(skillCount, 'input');
    fixture.detectChanges();

    submitButton.click();
    fixture.detectChanges();

    expect(component.showWorkout).toHaveBeenCalledWith('10');
  })
}));

Plunker Example

but wait why do we have to change our code?

3) Just add async to your beforeEach function

beforeEach(async(() => {
  fixture = TestBed.createComponent(StartworkoutComponent);
  component = fixture.componentInstance;
  element = fixture.debugElement;
  fixture.detectChanges();
}));

Plunker Example




回答2:


I think whenStable should do the trick in your case:

it('submits the value',() => {
    fixture.whenStable().then(() => {
        // ngModel should be available here 
    })
});

More information here: https://angular.io/docs/ts/latest/guide/testing.html#!#when-stable

EDIT: The ValueAccessor seems not to notice the change if you directly manipulate the value of the DOM-element. But it should notice, if you write the value directly with the ValueAccessor:

const skillCount= fixture.debugElement.query(By.directive(NgModel));
const ngModel= skillCount.injector.get(NgModel);

ngModel.valueAccessor.writeValue('10')


来源:https://stackoverflow.com/questions/42426891/angular2-ngmodel-not-getting-value-in-jasmine-test

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!