Writing End-to-End Tests for Javascript Web Applications - Introduction
After we have finished this lesson, we:
- Know the difference between a normal web application and a Javascript web application.
- Understand what we have to take into account when we write end-to-end tests for Javascript web applications.
- Can make the required changes to the components that are used to configure our end-to-end tests.
This lesson assumes that:
The Difference Between a Normal Web Application and a Javascript Web Application
Naturally a normal web application and a Javascript web application have quite many differences. However, if we think about end-to-end tests, the most crucial difference is that:
- A normal web application constructs the entire HTML document in the backend before it returns the HTML document back to the browser.
- The user interface of a Javascript web application consists of UI components that become visible when their data becomes available.
In other words, when we write end-to-end tests for a Javascript web application, we must follow these two rules:
- We have to ensure that we can interact with a UI component before we try to interact with it.
- We have to verify that the required information is visible before we write assertions for it.
Let’s move on and find out what kind of changes we have to make before we can configure our end-to-end tests.
Enabling and Disabling the HTML5 Mode of AngularJS
Because our web application uses AngularJS, we must be able to enable and disable the HTML5 mode of AngularJS because it affects to the URL addresses used by our web application.
If the HTML5 mode is disabled, our web application uses URL addresses which contain a hashbang. On the other hand, if the HTML5 mode is enabled, our application tries to use “regular” URL addresses which don’t contain a hashbang.
Additional Reading:
Before we can write end-to-end tests for our web application, we have to create our URL addresses in a way that supports both configuration options. We can do this by following these steps:
First, we have to add a new method to the WebDriverEnvironment class. This method reads the value of the webdriver.html5.mode.enabled system property, and returns true if the HTML5 mode is enabled and false otherwise. If the value of the webdriver.html5.mode.enabled system property is not found, this method throws an exception.
After we have added this method to the WebDriverEnvironment class, its source code looks as follows:
1public final class WebDriverEnvironment {
2
3 private WebDriverEnvironment() {}
4
5 public static String getBaseUrl() {
6 String baseUrl = System.getProperty("webdriver.base.url");
7 if (baseUrl == null) {
8 throw new RuntimeException("No base url found!");
9 }
10 return baseUrl;
11 }
12
13 public static boolean isHtml5ModeEnabled() {
14 String html5Mode = System.getProperty("webdriver.html5.mode.enabled");
15 if (html5Mode == null) {
16 throw new RuntimeException("HTML5 mode not found!");
17 }
18 return Boolean.valueOf(html5Mode);
19 }
20}
Second, we have to make one change to the buildFromPath() method of the WebDriverUrlBuilder class. If the HTML5 mode is disabled, we have to append the string: ‘#/’ to the environment-specific host name.
After we have made this change to the WebDriverUrlBuilder class, its source code looks as follows:
1public final class WebDriverUrlBuilder {
2
3 private WebDriverUrlBuilder() {}
4
5 public static String buildFromPath(String path, Object... params) {
6 if ((path == null) || path.isEmpty()) {
7 throw new IllegalArgumentException("Path must be given");
8 }
9
10 path = String.format(path, params);
11
12 String baseUrl = WebDriverEnvironment.getBaseUrl();
13 if (!baseUrl.endsWith("/")) {
14 baseUrl += "/";
15 }
16
17 if (!WebDriverEnvironment.isHtml5ModeEnabled()) {
18 baseUrl += "#/";
19 }
20
21 if (path.startsWith("/")) {
22 path = path.replaceFirst("/", "");
23 }
24
25 return baseUrl + path;
26 }
27}
Let’s move on and find out how we can write end-to-end tests that wait until a specific condition is met.
Waiting Until a Specific Condition Is Met
Because we are writing end-to-end tests for a Javascript web application, we cannot assume that every UI component is visible right after we have opened a view. That’s why most of our end-to-end tests must follow these steps:
- Perform an operation that makes changes to the DOM.
- Wait until the DOM has been changed.
- Write the required assertions.
Because it’s likely that many end-to-end tests will use similar wait conditions, it’s a good idea to create a utility class that helps us reduce duplicate code. We can create this utility class by following these steps:
First, create a final class called: SeleniumWait and ensure that we cannot create new SeleniumWait objects. After we have created the SeleniumWait class, its source code looks as follows:
1public final class SeleniumWait {
2
3 private SeleniumWait() {}
4}
Second, add a new constant called: DEFAULT_TIMEOUT_IN_SECONDS to our utility class and ensure that its value is five. After we have created this constant, the source code of the SeleniumWait class looks as follows:
1public final class SeleniumWait {
2
3 private static final long DEFAULT_TIMEOUT_IN_SECONDS = 5;
4
5 private SeleniumWait() {}
6}
Third, add two methods to our utility class:
- The
waitUntilElementIsClickable()method waits until the specified element can be clicked. - The
waitUntilElementIsVisible()method waits until the specified element is visible.
After we have added these methods to our utility class, its source code 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 waitUntilElementIsVisible(WebDriver browser,
21 By elementLocator) {
22 WebDriverWait wait = new WebDriverWait(browser,
23 DEFAULT_TIMEOUT_IN_SECONDS
24 );
25 wait.until(
26 ExpectedConditions.visibilityOfElementLocated(elementLocator)
27 );
28 }
29}
After we have created these utility methods, we have to make some changes to the page objects that are used to log the user in before a test method is run and log the user out after a test method has been run.
Logging the User In Before a Test Method Is Run
The biggest difference between the login form of our traditional web application and our Javascript web application is that if a user clicks the login button, our traditional web application submits the login form. However, our Javascript web application invokes the submitLoginForm() method that is found from the scope of our custom directive.
The HTML template that renders the login form looks as follows:
1<div id="login-error-alert"
2 class="alert alert-danger alert-dismissible"
3 ng-if="loginFailed">
4 <button type="button"
5 class="close"
6 data-dismiss="alert"
7 aria-hidden="true">×</button>
8 <span translate="directive.login.form.login.failed.error.message"></span>
9</div>
10<div ng-if="!currentUser.username">
11 <form id="login-form" role="form">
12 <div class="form-group">
13 <label for="email-address"
14 translate="directive.login.form.email.address.label">
15 </label>
16 <input id="email-address"
17 class="form-control"
18 name="username"
19 type="email"
20 placeholder="{{'email.address.placeholder' | translate}}"
21 ng-model="login.username">
22 </div>
23 <div class="form-group">
24 <label for="password"
25 translate="directive.login.form.password.label">
26 </label>
27 <input id="password"
28 class="form-control"
29 name="password"
30 type="password"
31 placeholder="{{'email.address.placeholder' | translate}}"
32 ng-model="login.password">
33 </div>
34 <button id="login-submit-button"
35 type="button"
36 class="btn btn-default"
37 translate="directive.login.form.submit.button.label"
38 ng-click="submitLoginForm()">
39 </button>
40 </form>
41</div>
42<div id="authenticated-user-error"
43 ng-if="currentUser.username"
44 translate="page.login.authenticated.user.error.message">
45</div>
Our end-to-end tests use the LoginPage class when they want to log a user in a before a test method is run. Because we have to ensure that the our end-to-end tests can interact with the login form before they try to log the user in, we have to make some changes to the LoginPage class.
We can make these changes by following these steps:
- Write a method that waits until our test can click the button which logs the user in. This method ensures that our end-to-end tests cannot enter the used email address and password before they can actually log the user in.
- Change the implementation of the
submitLoginForm()method. The new implementation of this method simply clicks the button that submits the login form. - Rename the
open()method toopenAsAnonymousUser()and ensure that it waits until the login page is open before it returns a newLoginPageobject. We have to rename this method because we will use a different wait logic when we open the login page as an authenticated user. - Change the implementation of the
login()method. The new implementation of this method waits until the task list page is open before it returns a newTaskListPageobject.
After we have made the required changes to the LoginPage class, its source code looks as follows:
1import org.openqa.selenium.By;
2import org.openqa.selenium.WebDriver;
3
4public final class LoginPage {
5
6 private final WebDriver browser;
7 private final String pageUrl;
8
9 public LoginPage(WebDriver browser) {
10 this.browser = browser;
11 this.pageUrl = WebDriverUrlBuilder.buildFromPath("/user/login");
12 }
13
14 public LoginPage openAsAnonymousUser() {
15 browser.get(pageUrl);
16 waitUntilPageIsOpen();
17
18 return new LoginPage(browser);
19 }
20
21 public TaskListPage login(String emailAddress, String password) {
22 typeEmailAddress(emailAddress);
23 typePassword(password);
24 submitLoginForm();
25
26 TaskListPage shown = new TaskListPage(browser);
27 shown.waitUntilPageIsOpen();
28
29 return shown;
30 }
31
32 public void waitUntilPageIsOpen() {
33 SeleniumWait.waitUntilElementIsClickable(browser,
34 By.id("login-submit-button")
35 );
36 }
37
38 private void typeEmailAddress(String emailAddress) {
39 browser.findElement(By.id("email-address")).sendKeys(emailAddress);
40 }
41
42 private void typePassword(String password) {
43 browser.findElement(By.id("password")).sendKeys(password);
44 }
45
46 private void submitLoginForm() {
47 browser.findElement(By.id("login-submit-button")).click();
48 }
49}
The waitUntilPageIsOpen() method of the TaskListPage class waits until the whole task list page is shown to the web browser. We will skip the implementation of this method because we will talk more about it in the next lesson of this topic.
Next, we will make the required changes to the page object that allows us to log the user out after a test method has been run.
Logging the User Out After a Test Method Has Been Run
Our end-to-end tests use the NavigationBar class when they want to log the user out after a test method has been run. We have to rewrite the logUserOut() method because our new user interface doesn’t have a log out form. Instead, the logged in user is logged out when he clicks the log out button.
The HTML template that renders the log out button looks as follows:
1<ul class="nav navbar-nav navbar-right" ng-if="currentUser.username">
2 <li>
3 <button id="log-out-button"
4 type="button"
5 class="btn btn-default navbar-btn"
6 translate="directive.logout.button.button.label"
7 ng-click="logOut()">
8 </button>
9 </li>
10</ul>
We can rewrite this method by following these steps:
- Throw an exception if the log out button is not visible on the user interface.
- Click the log out button.
- Wait until the login page is open.
- Return a new
LoginPageobject.
After we have made the required changes to the NavigationBar class, its source code looks as follows:
1import org.openqa.selenium.By;
2import org.openqa.selenium.WebDriver;
3
4public final class NavigationBar {
5
6 private final WebDriver browser;
7
8 public NavigationBar(WebDriver browser) {
9 this.browser = browser;
10 }
11
12 public LoginPage logUserOut() {
13 if (browser.findElements(By.id("log-out-button")).isEmpty()) {
14 throw new RuntimeException("The logout button is not visible");
15 }
16
17 browser.findElement(By.id("log-out-button")).click();
18
19 LoginPage shown = new LoginPage(browser);
20 shown.waitUntilPageIsOpen();
21 return shown;
22 }
23}
We have now made the required changes to the components that are used to configure our end-to-end tests. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We have to ensure that we can interact with a UI component before we try to interact with it.
- We have to verify that the required information is visible before we write assertions for it.
- We should put our wait logic to a utility class because this helps us to eliminate duplicate code.