45

I would like to change the value of an input field from within an Angular 2 unit test.

<input type="text" class="form-control" [(ngModel)]="abc.value" />

I can't just change the ngModel because abc object is private:

 private abc: Abc = new Abc();

In Angular 2 testing, can I simulate the user typing into the input field so that the ngModel will be updated with what the user has typed from within a unit test?

I can grab the DebugElement and the nativeElement of the input field without a problem. (Just setting a the value property on the nativeElement of the input field doesn't seem to work as it doesn't update the ngModel with what I've set for the value).

Maybe inputDebugEl.triggerEventHandler can be called, but I'm not sure what arguments to give it so it will simulate the user having typed a particular string of input.

3 Answers 3

55

You're right that you can't just set the input, you also need to dispatch the 'input' event. Here is a function I wrote earlier this evening to input text:

function sendInput(text: string) {
  inputElement.value = text;
  inputElement.dispatchEvent(new Event('input'));
  fixture.detectChanges();
  return fixture.whenStable();
}

Here fixture is the ComponentFixture and inputElement is the relevant HTTPInputElement from the fixture's nativeElement. This returns a promise, so you'll probably have to resolve it sendInput('whatever').then(...).

In context: https://github.com/textbook/known-for-web/blob/52c8aec4c2699c2f146a33c07786e1e32891c8b6/src/app/actor/actor.component.spec.ts#L134


Update:

We had some issues getting this to work in Angular 2.1, it didn't like creating a new Event(...), so instead we did:

import { dispatchEvent } from '@angular/platform-browser/testing/browser-util';

...

function sendInput(text: string) {
  inputElement.value = text;
  dispatchEvent(fixture.nativeElement, 'input');
  fixture.detectChanges();
  return fixture.whenStable();
}
Sign up to request clarification or add additional context in comments.

10 Comments

Thank you very much Jon. This worked great and the ngModel gets updated with the desired text. You've really saved me a bunch of time with your answer.
Are there any other gotchas you found when implementing this? I'm trying to get this solution to work, but so far haven't been able to get the model to update. I'm on Angular 2.1.2, so not sure if something broke / changed since this post.
@NathanG not that I recall. Trying more or fewer change detections and waits for stability may help.
@NathanG they definitely changed something. I am not able to do anything with that as well. I assume that test zone does not react on the event anymore, this is the only explanation I could come with. Still no solution
I decided to test this theory out by reverting my project to use the Angular version that @jonrsharpe used for his example, but this did not change the result. So perhaps I am doing something wrong. I'm going to provide some images to my code in hopes someone spots something obviously incorrect:
|
22

The accepted solution didn't quite work for me in Angular 2.4. The value I had set was not appearing in the (test) UI, even after detectChanges() was called.

The way I got it to work was to set up my test as follows:

describe('TemplateComponent', function () {
  let comp: TemplateComponent;
  let fixture: ComponentFixture<TemplateComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ FormsModule ],
      declarations: [ TemplateComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TemplateComponent);
    comp = fixture.componentInstance;
  });

  it('should allow us to set a bound input field', fakeAsync(() => {
    setInputValue('#test2', 'Tommy');

    expect(comp.personName).toEqual('Tommy');
  }));

  // must be called from within fakeAsync due to use of tick()
  function setInputValue(selector: string, value: string) {
    fixture.detectChanges();
    tick();

    let input = fixture.debugElement.query(By.css(selector)).nativeElement;
    input.value = value;
    input.dispatchEvent(new Event('input'));
    tick();
  }
});

My TemplateComponent component has a property named personName in this example, which was the model property I am binding to in my template:

<input id="test2" type="text" [(ngModel)]="personName" />

5 Comments

This answer ended 3 hours of pain for me
For this to work for me, after calling setInput, I had to use a fixture.whenStable( () => { expect(...) }); block.
Just a note @redOctober13 that instead of using a fixture.whenStable(), you should use a call to flush() (in Angular 4.2+) or tick() (in earlier versions). .whenStable() is designed to be used with async(), not fakeAsync(). My team spent hours chasing down odd test behavior (i.e. tests only passing some of the time) when we violated this rule at one point.
Thanks @paul, good to know. Did you find that documented anywhere?
@redOctober13 I haven't seen it explicitly documented. But experience has taught my team to avoid it. The whole point of fakeAsync afaik is to enable a synchronous-like test to avoid needing whenStable. The Angular.io site's testing page gives a good summary of the intended uses and they never combine fakeAsync and whenStable. Maybe that's proof enough....
6

I also had trouble getting jonrsharpe's answer to work with Angular 2.4. I found that the calls to fixture.detectChanges() and fixture.whenStable() caused the form component to reset. It seems that some initialization function is still pending when the test starts. I solved this by adding extra calls to these methods before each test. Here is a snippet of my code:

beforeEach(() => {
    TestBed.configureTestingModule({
        // ...etc...
    });
    fixture = TestBed.createComponent(LoginComponent);
    comp = fixture.componentInstance;
    usernameBox = fixture.debugElement.query(By.css('input[name="username"]'));
    passwordBox = fixture.debugElement.query(By.css('input[type="password"]'));
    loginButton = fixture.debugElement.query(By.css('.btn-primary'));
    formElement = fixture.debugElement.query(By.css('form'));
});

beforeEach(async(() => {
    // The magic sauce!!
    // Because this is in an async wrapper it will automatically wait
    // for the call to whenStable() to complete
    fixture.detectChanges();
    fixture.whenStable();
}));

function sendInput(inputElement: any, text: string) {
    inputElement.value = text;
    inputElement.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    return fixture.whenStable();
}

it('should log in correctly', async(() => {

    sendInput(usernameBox.nativeElement, 'User1')
    .then(() => {
        return sendInput(passwordBox.nativeElement, 'Password1')
    }).then(() => {
        formElement.triggerEventHandler('submit', null);
        fixture.detectChanges();

        let spinner = fixture.debugElement.query(By.css('img'));
        expect(Helper.isHidden(spinner)).toBeFalsy('Spinner should be visible');

        // ...etc...
    });
}));

1 Comment

Even though I don't use whenStable, the magic sauce hint helped me out of my misery: My input field mysteriously remained empty during the test. Adding async to beforeEach(async(() => { ... component = fixture.componentInstance; fixture.detectChanges(); })); made the field finally be filled.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.