Brice Argenson
Brice Argenson

Reputation: 832

Angular testing and ngModel

I'm writing one of my first component test with Angular and I have some difficulties to make the ngModel binding work. Here is my test module definition:

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        LdapLoginComponent,
      ],
      imports: [
        CommonModule,
        FormsModule,
        NoopAnimationsModule,
        MatInputModule,
        MatFormFieldModule,
        RouterTestingModule,
      ],
      providers: [
        {
          provide: AuthorizationService,
          useValue: { login() {} },
        },
      ]
    }).compileComponents();
  }));

And here my test case:

it('should bind form fields with class', fakeAsync(() => {
    // Given
    const username = 'username';
    const password = 'password';
    const usernameField = de.query(By.css('input[name=username]')).nativeElement;
    const passwordField = de.query(By.css('input[name=password]')).nativeElement;

    // When
    usernameField.value = username;
    passwordField.value = password;
    usernameField.dispatchEvent(new Event('input'));
    passwordField.dispatchEvent(new Event('input'));
    tick();
    fixture.detectChanges();

    // Then
    expect(comp.username).toEqual(username);
    expect(comp.password).toEqual(password);
  }));

My component class:

export class LdapLoginComponent {

  username: string;
  password: string;
  errorMessage: string;
  submitDisabled = false;

  constructor(
    private authorizationService: AuthorizationService,
    private router: Router,
  ) {
  }

  login(): void {
    delete this.errorMessage;
    this.submitDisabled = true;
    this.authorizationService.login(AuthorizationProvider.LDAP, this.username, this.password)
      .subscribe(
        () => {
          this.router.navigate(['/']);
        },
        (err: Error) => {
          this.errorMessage = err.message;
          this.submitDisabled = false;
        },
      );
  }

}

And my component template:

<form class="form-container" (submit)="login()">
  <mat-form-field color="warn">
    <input
    matInput
    type="text"
    name="username"
    placeholder="Insert your username"
    [(ngModel)]="username"
    required
    i18n-placeholder="@@input.placeholder.username">
  </mat-form-field>
  <mat-form-field color="warn">
    <input
      matInput
      type="password"
      name="password"
      placeholder="Insert your password"
      [(ngModel)]="password"
      required
      i18n-placeholder="@@input.placeholder.password">
  </mat-form-field>
  <button
    mat-raised-button
    type="submit"
    color="warn"
    [disabled]="submitDisabled"
    i18n="@@input.submit">Submit</button>
</form>
<article>{{errorMessage}}</article>

I'm changing the value of the username and password fields inside my test and I'm expecting the username and password fields of my class to be updated accordingly. Everything works well if I test manually in my browser, but not in the test.

Any ideas ?

Thanks.

Upvotes: 6

Views: 4202

Answers (2)

Krishna Chaitanya P
Krishna Chaitanya P

Reputation: 1314

Looks like for mat-input fields, we need to do a focus() before changing the input:

it('should bind form fields with class', fakeAsync(() => {
    // Given
    const username = 'username';
    const password = 'password';
    const usernameField = de.query(By.css('input[name=username]')).nativeElement;
    const passwordField = de.query(By.css('input[name=password]')).nativeElement;

    usernameField.focus();
    passwordField.focus();

    // When
    usernameField.value = username;
    passwordField.value = password;
    usernameField.dispatchEvent(new Event('input'));
    passwordField.dispatchEvent(new Event('input'));
    tick();
    fixture.detectChanges();

    // Then
    expect(comp.username).toEqual(username);
    expect(comp.password).toEqual(password);
}));

Upvotes: 3

Alistair Bates
Alistair Bates

Reputation: 41

The problem is that you are not actually setting the value of your input field before calling dispatchEvent. You are setting the component attribute directly.

comp.username = username;
usernameField.dispatchEvent(new Event('input'));

Should be

let usernameFieldElement = usernameField.nativeElement;
usernameFieldElement.value = username;
usernameField.dispatchEvent(new Event('input'));

and the same for password.

The other thing is you are testing three things at once, the entering of text into the input areas, clicking the button runs the logon and the logon function itself. I would suggest splitting these into three assertions.

  1. Set the fields as above and check the data binding.

  2. Click the button and spy on the login function to check it's been called.

  3. Set the actual component attributes and call logon() directly and check your navigateSpy has been called correctly.

This way if something goes wrong you will be able to find it a lot more easily.

Upvotes: 1

Related Questions