Writing End-to-End Tests for Javascript Web Applications – Rendering Data

After we have finished this lesson, we

  • Understand how we can select the “correct wait condition” when we are writing end-to-end tests for features that display data.
  • Know how we can make the required changes to our page objects.

Writing End-to-End Tests for a Feature That Displays Data

When we are writing end-to-end tests for a feature that displays data, typically we want to write two kinds of tests:

  • We want to ensure that the correct information is shown to the user.
  • We want to verify that the different HTML elements such as links and buttons are working as expected when we interact with them.

As we remember , we cannot trust that the entire HTML document is ready when we open the tested page, and that’s why we have to wait until the entire HTML document has been constructed. The question is:

How can we select the condition that must be true before our end-to-end test can continue?

Unfortunately, there is no universal answer to this question and we might have to try multiple conditions before we find the condition that doesn’t cause random test failures. That being said, it’s often a good idea to wait until the slowest API call has finished and its results have been rendered, or until we can perform the tested operation.

Let’s move on and take a look at few examples which demonstrate what I mean.

Rendering a Single Item

When we write end-to-end tests for the feature that displays the information of a single task, we want to ensure that:

  • The correct information is shown to the user.
  • The user can delete the shown task.
  • The user can navigate to the update task page.

The source code of view that displays the information of a single task looks as follows:

 1<h1 id="task-title">{{task.title}}</h1>
 2<div class="panel panel-default">
 3    <div class="panel-body">
 4        <div id="task-description"
 5             ng-if="!task.description"
 6             translate="page.view.task.no.description.message"></div>
 7        <div id="task-description" ng-if="task.description">
 8            {{task.description}}
 9        </div>
10    </div>
11</div>
12<div id="lifecycle-fields" class="container-fluid">
13    <div class="row" ng-if="task.assignee != null">
14        <div class="col-md-3">
15            <span translate="page.view.task.assignee.description"></span>:
16            <span id="assignee-name">{{task.assignee.name}}</span>
17        </div>
18    </div>
19    <div class="row">
20        <div class="col-md-3">
21            <span translate="page.view.task.creator.description.prefix">
22            </span>
23            <span id="creator-name">{{task.creator.name}}</span>
24            <span translate="page.view.task.timestamp.prefix"></span>
25            <span id="creation-time">
26                {{task.creationTime | amDateFormat:'YYYY-MM-DD H:mm:ss'}}
27            </span>
28        </div>
29        <div class="col-md-3">
30            <span translate="page.view.task.modifier.description.prefix">
31            </span>
32            <span id="modifier-name">{{task.modifier.name}}</span>
33            <span translate="page.view.task.timestamp.prefix"></span>
34            <span id="modification-time">
35                {{task.modificationTime | amDateFormat:'YYYY-MM-DD H:mm:ss'}}
36            </span>
37        </div>
38        <div id="closed-task-fields"
39             class="col-md-3"
40             ng-if="task.status == 'CLOSED'">
41            <span translate="page.view.task.closer.description.prefix">
42            </span>
43            <span id="closer-name">{{task.closer.name}}</span>
44            <span translate="page.view.task.timestamp.prefix"></span>
45            <span id="closing-time">
46                {{task.modificationTime | amDateFormat:'YYYY-MM-DD H:mm:ss'}}
47            </span>
48        </div>
49    </div>
50</div>
51<div id="task-action-links" class="row">
52    <div class="col-md-1">
53        <span delete-task-link task="task"></span>
54    </div>
55    <div class="col-md-1">
56        <a id="update-task-link"
57           ui-sref="task.update({id: task.id})"
58           translate="page.view.task.update.task.link.label">
59        </a>
60    </div>
61</div>

The next question is: what condition should we use when we wait until the entire HTML document has been constructed?

When we search the answer to this question, we notice that:

  • It’s clear that finding the the information of the shown task is the slowest (and only) API call that must be finished before our view can display the data of the found task.
  • We cannot write all end-to-end tests IF our tests cannot click the delete and update links.

So, how can we decide between these two options? Well, I simply opened the developer tools of my web browser and noticed that the entire page is rendered “at the same time”. That’s why I decided to wait until my tests can click the update task link.

We can implement this wait logic by making the following changes to the TaskPage class:

  1. Add a waitUntilPageIsOpen() method to the TaskPage class and implement this method by waiting until our tests can click the update task link.
  2. Change the implementation of the open() method. Our new implementation must wait until our tests can click the update task link before it returns a new TaskPage object.

After we have made these changes, the relevant part of the TaskPage class looks as follows:

 1import org.apache.commons.lang3.StringUtils;
 2import org.openqa.selenium.By;
 3import org.openqa.selenium.WebDriver;
 4
 5final class TaskPage {
 6
 7    private final WebDriver browser;
 8    private final String pageUrl;
 9    private final Long taskId;
10
11    TaskPage(WebDriver browser, Long taskId) {
12        this.browser = browser;
13        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/task/%d", taskId);
14        this.taskId = taskId;
15    }
16
17	//Other methods are omitted.
18
19    TaskPage open() {
20        if (taskId == null) {
21            throw new RuntimeException("Task id is not specified");
22        }
23
24        browser.get(pageUrl);
25        waitUntilPageIsOpen();
26        return new TaskPage(browser, taskId);
27    }
28
29    void waitUntilPageIsOpen() {
30        SeleniumWait.waitUntilElementIsClickable(browser,
31                By.id("update-task-link")
32        );
33    }
34}

Also, before we can write end-to-end tests that click the links found from the view task page, we have to make some changes to the TaskActions class which provides methods that allow us to click both delete and update task links.

We can make the change that is relevant to this lesson by changing the implementation of the updateTask() method. Our new implementation simply waits until the update task page is open before it returns a new UpdateTaskPage object.

After we have made this change, the relevant part of the TaskActions class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3import org.openqa.selenium.WebElement;
 4
 5final class TaskActions {
 6
 7    private final WebDriver browser;
 8    private final WebElement rootElement;
 9    private final Long taskId;
10
11    TaskActions(WebDriver browser, WebElement rootElement, Long taskId) {
12        this.browser = browser;
13        this.rootElement = rootElement;
14        this.taskId = taskId;
15    }
16
17    //We will talk about the delete method on another lesson.
18
19    UpdateTaskPage updateTask() {
20        WebElement updateTask = rootElement.findElement(
21                By.id("update-task-link")
22        );
23        updateTask.click();
24
25        UpdateTaskPage targetPage = new UpdateTaskPage(browser, taskId);
26        targetPage.waitUntilPageIsOpen();
27
28        return targetPage;
29    }
30}

There are two things that I want point out before we can continue this lesson:

  • We will skip the changes made to the UpdateTaskPage class because we will take a closer look at them in the next lesson of this topic.
  • We will skip the changes made to the deleteTask() method of the TaskActions class because we will talk about these changes in another lesson of this topic.

Next, we will find out how we can implement the wait logic when we are writing end-to-end tests for a feature that renders a list page.

Rendering a List

The view that renders the task list page has three requirements:

  • It must display a message that describes how many tasks were found from the database.
  • It must display the title and translated status of each task.
  • It must provide a link that opens the view task page.

The source code of the view that displays the task list page looks as follows:

1<h1 translate="page.task.list.title"></h1>
2<div id="task-list-size-message"
3     translate="page.task.list.size.message"
4     translate-values="{ taskCount: tasks.length }"></div>
5<div task-list tasks="tasks">
6</div>

The actual task list is rendered by a custom directive. This directive displays the information of each task and provides a link that opens the view task page. The source code of the HTML template that displays the task list looks as follows:

 1<table id="task-list"
 2       class="table table-striped"
 3       ng-if="tasks.length > 0">
 4    <thead>
 5    <tr>
 6        <th translate="task.property.title.header"></th>
 7        <th transtate="task.property.status.header"></th>
 8    </tr>
 9    </thead>
10    <tbody>
11    <tr id="task-list-item-{{task.id}}"
12        class="task-list-item"
13        ng-repeat="task in tasks">
14        <td>
15            <a class="task-list-item-title"
16               ui-sref="task.view({id: task.id})">{{task.title}}</a>
17        </td>
18        <td class="task-list-item-status task-status-{{task.status}}"
19            translate="TaskStatus.{{task.status}}">
20        </td>
21    </tr>
22    </tbody>
23</table>

When we write end-to-end tests for a feature that displays a list, typically we want to ensure that the list page displays the correct information. Also, in our case, we want to verify that we can open the view task page by clicking the link found from the task list page.

Also, when we take a look at these templates, we notice that finding the task list is the slowest (and only) API call that must be finished before our application can display the task list. It’s quite easy to select the correct wait condition because we cannot write our end-to-end tests if the entire task list is not visible. That’s why I think that we should wait until the information of all tasks is visible on the user interface.

We can implement this wait logic by following these steps:

First, we have to add a new method to the SeleniumWait class. This method waits until all specified elements are visible. After we have written this method, the source code of the SeleniumWait class looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3import org.openqa.selenium.support.ui.ExpectedConditions;
 4import org.openqa.selenium.support.ui.WebDriverWait;
 5
 6public final class SeleniumWait {
 7
 8    private static final long DEFAULT_TIMEOUT_IN_SECONDS = 5;
 9
10    private SeleniumWait() {}
11
12    public static void waitUntilElementIsClickable(WebDriver browser,
13                                                   By elementLocator) {
14        WebDriverWait wait = new WebDriverWait(browser,
15           DEFAULT_TIMEOUT_IN_SECONDS
16        );
17        wait.until(ExpectedConditions.elementToBeClickable(elementLocator));
18    }
19
20    public static void waitUntilAllElementsAreVisible(WebDriver browser,
21                                                      By elementLocator) {
22        WebDriverWait wait = new WebDriverWait(browser,
23           DEFAULT_TIMEOUT_IN_SECONDS
24        );
25        wait.until(
26           ExpectedConditions.visibilityOfAllElementsLocatedBy(elementLocator)
27        );
28    }
29
30    public static void waitUntilElementIsVisible(WebDriver browser,
31                                                 By elementLocator) {
32        WebDriverWait wait = new WebDriverWait(browser,
33           DEFAULT_TIMEOUT_IN_SECONDS
34        );
35        wait.until(
36           ExpectedConditions.visibilityOfElementLocated(elementLocator)
37        );
38    }
39}

Second, we have to make two changes to the TaskListPage class:

  1. Add a new method to the TaskListPage class. This method waits until all items, which are found from the database, are visible on the user interface.
  2. Change the implementation of the open() method. The new implementation of this method waits until the task list page is open before it returns a new TaskListPage object.

After we have made the required changes to the TaskListPage class, its source code looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3
 4import java.util.ArrayList;
 5import java.util.List;
 6
 7public final class TaskListPage {
 8
 9    private final WebDriver browser;
10    private final String pageUrl;
11
12    public TaskListPage(WebDriver browser) {
13        this.browser = browser;
14        this.pageUrl = WebDriverUrlBuilder.buildFromPath("/");
15    }
16
17    List<TaskListItem> getListItems() {
18        List<TaskListItem> listItems = new ArrayList<>();
19        browser.findElements(By.className("task-list-item"))
20                .forEach(e -> listItems.add(new TaskListItem(browser, e)));
21        return listItems;
22    }
23
24    public boolean isOpen() {
25        return browser.getCurrentUrl().equals(pageUrl);
26    }
27
28    public TaskListPage open() {
29        browser.get(pageUrl);
30        waitUntilPageIsOpen();
31
32        return new TaskListPage(browser);
33    }
34
35    public void waitUntilPageIsOpen() {
36		SeleniumWait.waitUntilAllElementsAreVisible(browser,
37				By.className("task-list-item")
38		);
39    }
40}

Third, we have to make one change to the TaskListItem class. The new implementation of the viewTask() method waits until the view task page is open before it returns a new TaskPage object.

After we have made this change so the TaskListItem class, its source code looks as follows:

 1import org.openqa.selenium.By;
 2import org.openqa.selenium.WebDriver;
 3import org.openqa.selenium.WebElement;
 4
 5final class TaskListItem {
 6
 7    private final WebDriver browser;
 8    private final WebElement rootElement;
 9
10    TaskListItem(WebDriver browser, WebElement rootElement) {
11        this.browser = browser;
12        this.rootElement = rootElement;
13    }
14
15    Long getId() {
16        String elementId = rootElement.getAttribute("id");
17        String taskId = elementId.replace("task-list-item-", "");
18        return Long.parseLong(taskId);
19    }
20
21    String getTitle() {
22        return rootElement.findElement(By.className("task-list-item-title"))
23                .getText();
24    }
25
26    TaskStatus getStatus() {
27        WebElement statusElement = rootElement.findElement(
28                By.className("task-list-item-status")
29        );
30
31        String status = statusElement.getAttribute("class")
32                .replace("task-list-item-status", "")
33                .trim()
34                .replace("task-status-", "");
35
36        return TaskStatus.valueOf(status);
37    }
38
39    TaskPage viewTask() {
40        WebElement viewTaskLink = rootElement.findElement(
41                By.className("task-list-item-title")
42        );
43
44        TaskPage targetPage = new TaskPage(browser, getId());
45
46        viewTaskLink.click();
47        targetPage.waitUntilPageIsOpen();
48
49        return targetPage;
50    }
51}

We have now made the changes that allow us run our “old” end-to-end tests which verify that the view task feature and the task list feature are working as expected. Also, it’s good to understand that we don’t have to make any changes to the actual end-to-end test classes.

Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • We might have to try multiple wait conditions before we find the condition that doesn’t cause random test failures.
  • If we want to ensure that the correct information is shown to the user, we have to wait until the slowest API call has finished and its results have been rendered.
  • If we want to verify that a user can perform an action (such as click to a link), we have to wait until the link is clickable.

Get the source code from Github