Testing is an integral part of our work and very few of our projects can do without automatic unit or integration tests. They help to comply with our quality standards, strengthen trust in the software and protect against unwanted side effects of planned or unplanned changes.

If we want to test graphical interfaces - for example a website or an Angular component - we will sooner or later have to deal with user input. In this article I show how keystrokes or paste events can be simulated in integration tests.

Sample application

My experience shows that the best way to learn is through practical examples. Therefore I have developed a small application for this article. How it works is quickly described: You can add entries to a task list by typing something in a text field and confirming it with the Enter key. Optionally, it is possible to insert a text from the clipboard. In both cases, the list will be expanded until a set limit is reached. After that, no more tasks can be added.

As the title of this article suggests, our TaskApp is an Angular application. Angular is a development platform for creating efficient and sophisticated applications based on TypeScript and is equipped with everything necessary for development and testing. The framework can play to its strengths especially in the enterprise environment, so it is understandable that our partners rely on Angular: SAP with Spartacus, the storefront of the SAP Commerce Cloud and CELUM with the Nova UI, the new user interface of the ContentHub.

Practical tip: Arrange, Act, Assert

We try to follow the AAA pattern wherever it makes sense. This pattern describes that a test should be divided into three logical sections: • arrangement, • acts • Assert.

Each of these areas is only responsible for the part it is named after. This pattern helps to structure tests better and make them easier to understand. Testing First we define our test cases. They are specified by the functionality of our application and ensure the basic functionality.

describe('Useless TaskApp', () => { 
  it('should add a task after pressing enter'); 
  it('should add a task using paste only if enabled'); 
  it('should update the task headline correctly'); 
  it('should ignore whitespace only tasks'); 
  it('should not add a task after the task limit was reached'); 
  it('should disable the textbox after the limit was reached'); 
  it('should update the task hint properly'); 
  it('should clear the textbox after a task was added using enter-key'); 
  it('should clear the textbox after a task was added using paste'); 
});

 

As a reminder, we want to make sure that text entered into a text box appears in the task list after pressing Enter. Let's go through the first test case as an example. 1. Find the textbox element 2. Add a text in the textbox element

  1. Find the textbox element
  2. Add a text in the textbox element
  3. Press Enter (simulated)
  4. heck if the task with the entered text appears in the list

In order to select DOM elements effectively and precisely, we use data-*-Attributes.. In this way, selectors can be selected independently of CSS classes and IDs, as these are often generated dynamically in modern applications. In addition, we achieve a certain independence from the development process, because adjustments to the tests are largely avoided when changes are made to the application code.

Example

it('should add a task after pressing enter', () => { 
   // Arrange 
   const tasktext = 'Make appointment'; 
   const taskbox = findElement(fixture, '[data-test-taskbox]'); // 1. 
   const input = taskbox as HTMLInputElement; 
 
   // Act 
   input.value = tasktext; // 2. 
   taskbox.triggerEventHandler('keydown.enter', {target: input}); // 3. 
   fixture.detectChanges(); 
 
   // Assert 
   const tasklist = findElement(fixture, '[data-test-tasklist]'); 
   expect(tasklist.nativeElement.textContent).toEqual(tasktext); // 4. 
});

 

The findElement() function is not part of the Angular testing tools but a small helper function to select elements. It returns an Angular DebugElement instance that provides a handy method for triggering events: triggerEventHandler().

triggerEventHandler()

When a listener is bound to an element, the event can be triggered based on its name. But this only works as long as we use Angular's ownEvent-Binding.

The so-called pseudo-events for keyboard inputs are exciting in this context. They are mentioned in the Angular documentation but not described in detail. Roughly speaking, pseudo-events are an abstraction layer around KeyboardEvent objects and allow you to react directly to a specific keystroke or key combination. This means that an event is only triggered with exactly this key or key combination and it is not necessary to check each time a key is pressed whether the key just pressed corresponds to what is expected. If you are interested, you can read the source code and the associated tests. the associated tests.

A helper function that implements triggerEventHandler might look like this:

function addByEnter(fixture: ComponentFixture<unknown>, text: string: void { 
  const element = findElement(fixture, '[data-test-taskbox]'); // 1. 
  const input = element.nativeElement as HTMLInputElement; // 2. 

  input.value = text; 
  element.triggerEventHandler('keydown.enter', { target: input }); // 3. 
  fixture.detectChanges(); 
}

 

  1. Find the element with attribute data-test-taskbox
  2. Get the DOM element
  3. Simulate the keystroke. It is important to specify the target in the event object correctly - in this case the HTMLInputElement

Native APIs

For cases where events unknown to Angular need to be handled, we have the native APIs at our disposal. In the following example we create a paste event and fire it.

function addByPaste(fixture: ComponentFixture<unknown>, text: string): void { 
  const element = findElement(fixture, '[data-test-taskbox]'); 
  const input = element.nativeElement as HTMLInputElement; 
  const clipboardEvent: Event = new Event('paste', {}); 

  clipboardEvent.clipboardData = { 
    getData: () => text 
  }; 

  input.dispatchEvent(clipboardEvent); 
  fixture.detectChanges(); 
}

 

Simulating user input in integration and unit tests is not difficult. You just have to know the right tools. Happy Testing! The source code can be downloaded from Githubor tried directly on Stackblitz.