0
votes

I would like to test a template driven form. Initially I would like the form's button to be disabled since I want to make sure the user enters a name. This is done by the 'required' property on the name input field.

So I created the following template:

<form #form="ngForm">
  <!-- note the 'required' property on the input field -->
  <input
    type="text"
    name="name"
    id="name"
    #name="ngModel"
    required
    ngModel
  />
  <button type="submit" [disabled]="form.invalid">Submit</button>
  {{ form.invalid | json }}
</form>

This renders nicely in the browser:

enter image description here

Now I would like to test this behavior. And here is where it gets funky. The test is as follows:

import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { DebugElement } from "@angular/core";
import { By } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";

describe("AppComponent", () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let el: DebugElement;
  let button: HTMLButtonElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      imports: [CommonModule, FormsModule]
    }).compileComponents();
  }));
  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    el = fixture.debugElement;
    fixture.detectChanges();
    button = el.query(By.css("button")).nativeElement;
  });
  describe("submit button", () => {
    it("should be disabled by default", () => {
      expect(button.nativeNode.disabled).toBeTrue();
    });
  });
});

My test fails and it does not recognise the form.invalid property. It sets it to 'false':

enter image description here

So my best guess would be that the actual 'ng build' or 'ng serve' of the Angular application does some magic that sets the ngForm to have an invalid property if one of the input values does not match the requirements.

But how do I do this in my test?

I would like the test to be as close to the real world example. So having to set form.invalid to true manually kinda defeats the purpose of the test.

Here's a Stackblitz example of the forementioned code: https://stackblitz.com/edit/angular-ivy-pfnkfc

1

1 Answers

1
votes

So it seems that there are some asynchronous tasks that need to complete (we await fixture.whenStable and then we need to call fixture.detectChanges(). This is one of the reasons why I use reactive forms almost always because it is much easier to test. You can test them just by the TypeScript code alone and you don't have to look at the HTML at all.

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let el: DebugElement;
  let button: HTMLButtonElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      imports: [FormsModule]
    }).compileComponents();
  }));
  beforeEach(async () => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    el = fixture.debugElement;
    fixture.detectChanges();
  });

  describe('submit button', () => {
    it('should be disabled by default', async () => {
      await fixture.whenStable(); // wait for all asynchronous tasks to complete
      fixture.detectChanges(); // call detectChanges again
      button = fixture.debugElement.query(By.css('button')).nativeElement;
      expect(button.disabled).toBe(true);
    });
  });
});

Things to note:

1.) Every time you call fixture.detectChanges, you need a new reference to fixture.debugElement.query... because the view changes every time you call fixture.detectChanges. If we were to keep using el like you have in the beforeEach, you would have an old reference to how the HTML looked like.

2.) You don't need to import CommonModule in Angular unit tests.