Interacting with Forms
By: Jared Fineman
When we think of forms, we may have visions of a stack of papers, piled on a waiting room table in a doctor’s office. However distant web forms may be from that concept, in terms of tediousness, a lot remains the same. Whether it be laying them out, styling them, or simply binding them, forms can be a chore in their own unique way and nothing changes when it comes to unit testing. Thankfully, by leveraging the Angular Unit Testing Library, we can streamline the process.
Let’s start by examining a unit test for the completion of a form:
it('should activate done button when complete’, async(() => { const fixture = TestBed.createComponent(LocationOptionsComponent); fixture.detectChanges(false); let inputs = fixture.debugElement .queryAll(By.css('input.form-ontrol:not(.filter-input)')); let combos = fixture.debugElement.queryAll(By.directive(TypeaheadComponent)); let doneBtn = fixture.debugElement .queryAll(By.css('.star-grid-header-buttoncontainer input'))[1]; ...
As we can see from the description of the unit test, the done button should only become active after the form is in a state of completion. We begin by querying the created component in order to find the form and all of its elements.
One thing to note is the power of the queryAll method of the Angular library; it is able to use a complex css selector to return a collection of DebugElement objects that represent the inputs of the form. Alternatively, the queryAll method can be passed a value type of directive or component as a parameter and it will return all corresponding matches. For more information about the query method, please see my previous article titled, "Demystifying Angular Unit Tests: Part 2: The Anatomy of an Angular Spec File".
At this point in the unit test, we have a collection of all the various inputs, some simple and others more complex, like drop-down menus or autocomplete fields. The challenge here is being able to simulate interactions with each component properly in order to test the various form dynamics. The following sections will illustrate how we locate and interact with each particular field, using the Angular library:
optionNameInput.nativeElement['value'] = 'Testing'; optionNameInput.nativeElement.dispatchEvent(new Event('input'));
We begin by examining a variable named optionNameInput in order to gain a better understanding behind some of the key concepts in unit testing forms. The optionNameInput, as well as the other field variables, are all of type DebugElement, a powerful class for unit testing. This class has a property called nativeElement, by which we can directly access and manipulate the field value in the DOM. This is insufficient, however, to activate Angular’s event detection. In order to do that, we will need to simulate an input event using the dispatchEvent method; doing so will then trigger data binding, validation, as well as any event listeners.
We have just successfully simulated an input event for our unit test, but how would we handle something more complex like selecting an option from a drop-down menu or an autocomplete field?
optionTypeInput.nativeElement.dispatchEvent(new Event('input')); activeInput.nativeElement.dispatchEvent(new Event('input'));
Similar to the input field, our autocomplete variables, optionTypeCombo and activeCombo, require an event to be fired in order to activate change detection, and in turn, expand the option menu. We handle this by first calling the dispatchEvent method, followed by a strange call to a method called whenStable.
fixture.whenStable().then(() => { ... }
As described in the Angular site documentation, whenStable is responsible for returning a promise after the completion of an asynchronous event. In order for us to catch the event of the autocomplete menu opening, we need to nest the continuation of our code within the returned promise of the whenStable call.
let locationOption = optionTypeCombo.query(By.css('button')).nativeElement; locationOption.dispatchEvent(new Event('click')); let activeOption = activeCombo.query(By.css('button')).nativeElement; activeOption.dispatchEvent(new Event('click'));
Now that it has open, we are ready to make a selection from the autocomplete menu. Using the query method of the DebugElement object, we are able to find the first autocomplete field option, represented by a button element. Once again, we call the dispatchEvent method, but this time passing in a click event, to simulate selecting the first option.
fixture.detectChanges(); expect(doneBtn.properties['disabled']).toBe(false);
After a quick call to detectChanges, which starts the Angular detection process, we are ready to check if the form is in a complete state, our primary indicator being an active done button. We have already queried and saved the done button as a variable of type DebugElement; using its properties value, we can now detect the disabled status of our button.
If you like this post, please like it and share it. In addition, if you haven’t checked out Infragistics Ignite UI for Angular, be sure to do so! They’ve got 50+ Material-based Angular components to help you code speedy web apps faster.