The User Interface - Continuous Enterprise Development in Java (2014)

Continuous Enterprise Development in Java (2014)

Chapter 10. The User Interface

Beauty is a sign of intelligence.

— Andy Warhol

To this point, we’ve focused entirely on elements that cannot be seen. In this chapter we bring everything home by exposing our backend services to the end user.

When it comes to Enterprise Java, we have our fill of options for display technologies. The Java EE Specification provides JavaServer Faces (JSF), a component-based framework for web applications. This approach takes advantage of server-side rendering: that is, the final response returned to the client is created on the server from source templates (typically Facelets).

In our GeekSeek example, however, we’ll be going off the beaten path a bit and rolling our own single-page application in pure HTML. The dynamic elements backed by data will be supplied via JavaScript calls to the backend via the RESTful interface we exposed earlier.

In general, our requirements remain simply to expose our operations in a human-consumable format.

Use Cases and Requirements

On a high level, we’re looking to allow a user to take advantage of the application’s primary purpose: we’d like to modify the state of our domain objects in a consistent fashion. We can state these:

§ As a User I should be able to Add/Change/Delete a Conference

§ As a User I should be able to Add/Change/Delete a Session to Conferences

§ As a User I should be able to Add/Change/Delete an Attachment to Sessions and Conferences

§ As a User I should be able to Add/Change/Delete a Venue (and attach to Conferences and Sessions)

Implementation

Our frontend is written using the popular JavaScript framework AngularJS: a framework that lets us extend the HTML syntax, write client-side components in the familiar Model, View, Controller (MVC) pattern, and allow for a two-way binding of our data models.

AngularJS has a built-in abstraction to work with Resources like REST, but it lacks built-in support for HATEOAS. The AngularJS Resource can easily operate on a single Resource, but with no automatic link support or knowledge of the OPTIONS that the Resource might support. For our frontend view to be completely driven by the backend services, we need this extra layer of support.

To address HATEOAS in the client view we’ve created a simple object we call RestGraph. The main responsibility of this object is to discover linked Resources in the Response and determine what we’re allowed to do with the given Resource.

Without exposing too much of how exactly the RestGraph is implemented, we’ll just give you a short overview of what it can do and how it is useful.

To start, the RestGraph requires you to define a root Resource URL, the top-level Resource of the graph:

var root = RestGraph('http://geekseek.continuousdev.org/api').init();

This is similar to how it’s done when you visit a web page in a web browser; you give yourself and the browser a starting point by typing an address (URL) in the address bar.

Based on the Response from the root Resource we can determine what can be done next:

{

"link": [

{

"rel": "conference",

"href": "http://geekseek.continuousdev.org/api/conference",

"mediaType": "application/vnd.ced+json; type=conference"

},

{

"rel": "whoami",

"href": "http://geekseek.continuousdev.org/api/security/whoami",

"mediaType": "application/vnd.ced+json; type=user"

}

]

}

In this example the start Resource only contains links to other resources and contains no data itself. We can choose to discover where to go next by looking at all available links. Maybe let the user decide what path to take?

var paths = root.links

Or choose to follow the graph down a desired path by fetching a named relation:

var conference = root.getLink('conference')

Some function calls on the conference instance can be mapped directly to the REST verbs for the given Resource: GET, DELETE, PATCH, PUT:

conference.get()

conference.remove()

conference.add({})

conference.update({})

Now we have the basics. But, we still don’t know if we’re allowed to perform all of those operations on all discovered Resources. Certain security constraints and possible other limitations are implemented on the server side that we need to take into consideration before/when we make aRequest. In the same way we can discover related Resources and Actions via links in the Response, we can query the Resource for the OPTIONS it supports:

> OPTIONS /api/conference

< Allow: GET, OPTIONS, HEAD

If the user performing the Request is authenticated, the Response to the OPTIONS query might look like this:

> OPTIONS /api/conference

< Allow: GET, PATCH, OPTIONS, HEAD

The server just told us that an authenticated user is allowed to perform a PATCH operation on this Resource as well as a GET operation. Now the user is no longer restricted to a read-only view, but has full read/write access.

The RestGraph hides the usage of OPTIONS to query the server for allowed actions behind a meaningful API:

conference.canGet()

conference.canUpdate()

conference.canRemove()

conference.canCreate()

This gives us the complete picture of what the API and the backend, under the current circumstances, will allow us to do. While the communication with the backend is up and running, the user still can’t see anything. We need to convert the raw data into a suitable user interface.

We choose to map the MediaTypes described in the DAP to HTML templates in the UI:

{

"rel": "conference",

"href": "http://geekseek.continuousdev.org/api/conference",

"mediaType": "application/vnd.ced+json; type=conference"

}

By combining the current Action (View, Update, Create) with the MediaType subtype argument, we can identify a unique template for how to represent the current Resource as HTML.

As an example, the Conference MediaType in View mode could look like this:

<div class="single" data-ng-if="isSingle">

<div class="well">

<div class="pull-right">

<a data-ng-show="resource.canUpdate()" data-ng-click="edit()">

<i class="icon-edit-sign"></i></a>

<a data-ng-show="resource.canRemove()" data-ng-click="remove()">

<i class="icon-remove-sign"></i></a>

</div>

<h1>{{resource.data.name}} <small>{{resource.data.tagLine}}</small></h1>

<p class="date">

<abbr title="{{resource.data.start|date:medium}}" class="start">

<span class="day">{{resource.data.start|date:'d'}}</span>

</abbr>

<span class="sep">-</span>

<abbr title="{{resource.data.end|date:medium}}" class="end">

<span class="day">{{resource.data.end|date:'d'}}</span>

<span class="month">{{resource.data.end|date:'MMMM'}}</span>

<span class="year">{{resource.data.end|date:'yyyy'}}</span>

</abbr>

</p>

<div class="attendees pull-right">

<subresource parent="resource" link="attendees" />

</div>

</div>

<subresource parent="resource" link="session" />

</div>

Requirement Test Scenarios

The UI for our GeekSeek application is based on a JavaScript frontend talking to a REST backend. In this scenario, there are some different approaches and types of testing we can do: one is for the pure JavaScript code (e.g., client controllers) and the other part is the interaction with the browser and REST endpoints on the backend.

Pure JavaScript

For the pure client JavaScript we’re going to use QUnit, a JavaScript Unit Testing framework. And handily enough, Arquillian has an extension that can invoke QUnit execution within our normal Java build system.

Although the QUnit tests themselves do not require any Java code, the Arquillian QUnit extension uses a normal JUnit test class to configure and report on the QUnit execution.

Our UI code contains a graph that can hold the state of the various REST responses and their links. In this test scenario we want to test that the graph can understand the response returned from a REST service given an OPTIONS request.

We start by configuring the QUnit Arquillian runner in a simple JUnit Java class:

@RunWith(QUnitRunner.class)

@QUnitResources("src")

public class GraphTestCase {

@QUnitTest("test/resources/assets/tests/graph/graph-assertions.html")

public void testGraph() {

// empty body

}

}

In this example we introduce two new annotations that are specific to the Arquillian QUnit extension:

§ @QUnitResources defines the root source of the JavaScript files.

§ @QUnitTest defines which HTML page to run for this @Test.

The graph-assertions.html referenced in the @QUnitTest annotation is the HTML page that contains the <script> tag, which includes the QUnit JavaScript tests and any other JavaScript dependencies we might need:

<html>

<head>

<title>QUnit Test Suite</title>

<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css"

type="text/css" media="screen">

<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>

<script type="text/javascript"

src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular.js">

</script>

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.js">

</script>

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-mocks.js">

</script>

<script type="text/javascript"

src="../../../../../main/resources/META-INF/resources/webjars/core/graph.js">

</script>

<script type="text/javascript" src="assert.js"></script>

</head>

<body>

<h1 id="qunit-header">QUnit Test Suite</h1>

<h2 id="qunit-banner"></h2>

<div id="qunit-testrunner-toolbar"></div>

<h2 id="qunit-userAgent"></h2>

<ol id="qunit-tests"></ol>

</body>

</html>

Our assert.js is then free to contain the QUnit functions that define our client-side test suite:

module("Service OPTIONS", optionsInit)

asyncTest("can get?", 1, function() {

this.$initGraph('GET', function(node) {

ok(node.canGet(), "Should be able to create Resource")

})

});

asyncTest("can remove?", 1, function() {

this.$initGraph('DELETE', function(node) {

ok(node.canRemove(), "Should be able to remove Resource")

})

});

When we execute the GraphTestCase Java class as part of the test execution, Arquillian QUnit will create and configure Drone and Graphene to represent our defined environment. It then parses the QUnit JavaScript to extract the real test names and replace the Java JUnit defined ones. That means that in our test results we’ll see test names like “can remove?” and “can get?” as opposed to “testGraph.”

We have configured Drone to use the PhantomJS browser; this headless browser allows us to run on a CI server without a graphical environment. This is easily configurable via arquillian.xml.

With this setup we now have control over our JavaScript client code and can integrate JavaScript tests in our test pipeline.

Functional Behavior

We still have functional behavior in our application that goes beyond how the JavaScript code itself runs. Are the page elements displaying properly? Does the end user see what is expected?

One could argue that we’re now moving over from integration into functional testing. Either way, we need to set up our functional tests to be maintainable, robust, and easy to read.

We use Drone to control the lifecycle of the browser and Graphene to wrap the browser and provide client-side object injection.

We rely on a pattern called PageObjects from Selenium to encapsulate the logic within a page in a type-safe and programmable API. With Graphene we can take the PageObject concept one step further and use PageFragments. PageFragments are reusable components that you might find within a page. We might have a Conference object displayed on multiple different pages or a Login controller repeated in all headers.

By encapsulating the references to the HTML IDs and CSS rules within PageObjects and PageFragments, we can create reusable TestObjects that represent our application.

We start out by creating a PageObject for our application in org.cedj.geekseek.test. functional.ui.page.MainPage:

@Location("app/")

public class MainPage {

@FindBy(id = "action-links")

private ActionLinks actionLinks;

@FindBy(id = "user-action-links")

private ActionLinks userActionLinks;

@FindBy(id = "resource")

private WebElement resource;

public ActionLinks getActionLinks() {

return actionLinks;

}

public ActionLinks getUserActionLinks() {

return userActionLinks;

}

...

}

We use Graphene’s @Location to define the relative URL where this page can be found. By combining Graphene with Drone we can now simply inject the MainPage object into our @Test method. The injection will carry the state navigated to the correct URL and be fully powered byWebDriver in the background. With this arrangement, our test class will end up with the following structure:

@RunWith(Arquillian.class)

public class MyUITest {

@Drone

private WebDriver driver;

@Test

public void testSomething(@InitialPage MainPage page) { ...}

The testSomething method accepts a MainPage object with proper state intact.

When Graphene initializes the MainPage instance for injection, it scans the PageObject for @FindBy annotations to inject proxies that represent the given element. In our case we use a second layer of abstraction, ActionLinks, which is our PageFragment. Each page has a menu of “what can be done next?” following the flow of the underlying REST backend. These are split in two; actionLinks and userActionLinks. The differentiator: is this a general action against a Resource or an action against a Resource that involves the User? An example of an action is Add Conference, and a User action example would be Add me as a Tracker to this Conference.

We add an ActionLinks abstraction to simply expose a nicer API around checking if a link exists and how to retrieve it:

public class ActionLinks {

@Root

private WebElement root;

@FindBy(tagName = "button")

private List<WebElement> buttons;

public WebElement getLink(String name) {

for(WebElement elem : buttons) {

if(elem.getText().contains(name) && elem.isDisplayed()) {

return elem;

}

}

return null;

}

public boolean hasLink(String name) {

return getLink(name) != null;

}

}

The ActionLinks PageFragment is very similar in how the PageObject works. The main difference is the use of the @Root annotation. Both Actions and UserActions are modeled as the PageFragment type ActionLinks. They are two lists of links in different locations on the page. In the PageObject MainPage we have the following two injection points:

@FindBy(id = "action-links")

private ActionLinks actionLinks;

@FindBy(id = "user-action-links")

private ActionLinks userActionLinks;

The ActionsLinks @Root WebElement is injected based on the parent @FindBy element and represents where on the page this fragment was found. When working within a PageFragment, all of our @FindBy expressions are relative to the @Root element.

You might remember that our application is a single page, so everything happens within the same physical URL and the content is only manipulated via JavaScript. With this in mind we’ve modeled in a concept of a fragment being SelfAware. This allows us to encapsulate the logic of knowing how to find certain fragments within the fragment itself:

org.cedj.geekseek.test.functional.ui.page.SelfAwareFragment:

public interface SelfAwareFragment {

boolean is();

}

The MainPage PageObject implements the discovery logic like so:

public <T extends SelfAwareFragment> boolean isResource(Class<T> fragment) {

try {

return getResource(fragment).is();

} catch (NoSuchElementException e) {

return false;

}

}

public <T extends SelfAwareFragment> T getResource(Class<T> fragment) {

return PageFragmentEnricher.createPageFragment(fragment, resource);

}

Within the MainPage we want to set up PageFragments so they can be created dynamically based on the requested type. This is to avoid having to create a @FindBy injection point for all possible combinations within our application. But we still want our on-demand PageFragments to have the same features as the injected ones, so we delegate the actual creation of the instance to Graphene’s PageFragmentEnricher, giving it the requested type and the @Root element we expect it to be found within.

After discovering and executing ActionLinks we can now ask the MainPage: “Are we within a given subpage?” by referring just to the class itself:

public static class Form implements SelfAwareFragment {

@Root

private WebElement root;

@FindBy(css = ".content.conference")

private WebElement conference;

@FindBy(tagName = "form")

private WebElement form;

@FindBy(css = "#name")

private InputComponent name;

...

@FindBy(tagName = "button")

private List<WebElement> buttons;

@Override

public boolean is() {

return conference.isDisplayed() && form.isDisplayed();

}

public Form name(String name) {

this.name.value(name);

return this;

}

public InputComponent name() {

return name;

}

...

public void submit() {

for(WebElement button : buttons) {

if(button.isDisplayed()) {

button.click();

break;

}

}

}

}

As shown in this example in one of our SelfAwareFragment types, Conference.Form, we continue nesting PageFragments to encapsulate more behavior down the stack (mainly the InputComponent). Whereas an HTML Form <input> tag knows how to input data, theInputComponent goes a level up:

textfield.html:

<div class="col-md-8 form-group" data-ng-class="{'has-error':error}">

<label class="control-label" for="{{id}}_field">{{name}}</label>

<input class="form-control" type="text" id="{{id}}_field"

data-ng-model="field"

required placeholder="{{help}}" />

<div class="has-error" data-ng-show="error">{{error}}</div>

</div>

The complete state of the input is required—not only where to put data, but also the defined name, “help” text, and, most importantly, is it in an error state after submitting?

We also have a custom extension to Drone and Arquillian: we need to ensure that “click” and “navigate” events wait for the loading of async calls before doing their time check. For this, we have theorg.cedj.geekseek.test.functional.arquillian.AngularJSDroneExtension, which defines:

public static class AngularJSEventHandler

extends AbstractWebDriverEventListener {

@Override

public void afterNavigateTo(String url, WebDriver driver) {

waitForLoad(driver);

}

@Override

public void afterNavigateBack(WebDriver driver) {

waitForLoad(driver);

}

@Override

public void afterNavigateForward(WebDriver driver) {

waitForLoad(driver);

}

@Override

public void afterClickOn(WebElement element, WebDriver driver) {

waitForLoad(driver);

}

private void waitForLoad(WebDriver driver) {

if(JavascriptExecutor.class.isInstance(driver)) {

JavascriptExecutor executor = (JavascriptExecutor)driver;

executor.executeAsyncScript(

"var callback = arguments[arguments.length - 1];" +

"var el = document.querySelector('body');" +

"if (window.angular) {" +

"angular.element(el).injector().get('$browser').

notifyWhenNoOutstandingRequests(callback);" +

"} else {callback()}");

}

}

}

The waitForLoad method, triggered by all of the action handlers, contains the logic to wait on an async call to return.

With all the main abstractions in place, we are now free to start validating the application’s functional 'margin-top:0cm;margin-right:0cm;margin-bottom:0cm; margin-left:18.0pt;margin-bottom:.0001pt;text-indent:-18.0pt;line-height:normal; vertical-align:baseline'>§ Given the User is Creating a new Conference

§ When the Conference has no start/end date

§ Then an error should be displayed

To satisfy these test requirements we have, for example, org.cedj.geekseek.test.functional.ui.AddConferenceStory:

@RunWith(Arquillian.class)

public class AddConferenceStory {

@Drone

private WebDriver driver;

@Test @InSequence(1)

public void shouldShowErrorMessageOnMissingDatesInConferenceForm(

@InitialPage MainPage page) {

ActionLinks links = page.getActionLinks();

Assert.assertTrue(

"Add Conference action should be available",

links.hasLink("conference"));

links.getLink("conference").click();

Assert.assertTrue(

"Should have been directed to Conference Form",

page.isResource(Conference.Form.class));

Conference.Form form = page.getResource(Conference.Form.class);

form

.name("Test")

.tagLine("Tag line")

.start("")

.end("")

.submit();

Assert.assertFalse("Should not display error", form.name().hasError());

Assert.assertFalse(

"Should not display error", form.tagLine().hasError());

Assert.assertTrue(

"Should display error on null input", form.start().hasError());

Assert.assertTrue(

"Should display error on null input", form.end().hasError());

}

The shouldShowErrorMessageOnMissingDatesInConferenceForm test method takes the following actions:

§ Go to the MainPage (as injected).

§ Get all ActionLinks.

§ Verify there is an ActionLink named conference.

§ Click the conference ActionLink.

§ Verify we’re on the Conference.Form.

§ Input given data in the form and submit it.

§ Verify that name and tagLine input are not in error state.

§ Verify that start and end input are in error state.

As we can see, Arquillian Drone, together with Selenium and QUnit, makes for an integrated solution to testing frontend code with a Java object model. Running the full suite on your own locally should be instructive.