Documentation

You are viewing the documentation for the 2.2.0 release in the 2.2.x series of releases. The latest stable release series is 3.0.x.

§Invoking actions from Javascript

In the last chapter of the tutorial, we implemented a number of new actions for the navigation drawer that are served by the backend. In this chapter we’ll add the client side code necessary to complete the behavior for the navigation drawer.

§Javascript routes

The first thing we need to do is implement a Javascript router. While you could always just make AJAX calls using hard coded URLs, Play provides a client side router that will supply these URLs for your AJAX requests. Building URLs to make AJAX calls can be quite fragile, and if you change your URL structure or parameter names at all, it can be easy to miss things when you update your Javascript code. For this reason Play has a Javascript router that lets us call actions on the server from Javascript; as if we were invoking them directly.

A Javascript router needs to be generated from our code to say what actions it should include. It can be implemented as a regular action that your client side code can download using a script tag. Alternatively, Play has support for embedding the router in a template. For now though we’ll just use the action method. Write a Javascript router action in app/controllers/Application.java:

public static Result javascriptRoutes() {
    response().setContentType("text/javascript");
    return ok(
        Routes.javascriptRouter("jsRoutes",
            controllers.routes.javascript.Projects.add(),
            controllers.routes.javascript.Projects.delete(),
            controllers.routes.javascript.Projects.rename(),
            controllers.routes.javascript.Projects.addGroup()
        )
    );
}

We’ve set the response content type to be text/javascript because the router will be a Javascript file. We’ve then used Routes.javascriptRouter() to generate the routes. The first parameter that we’ve passed to it is jsRoutes. Our Javascript/CoffeeScript code will be able to access the router using that variable name. We’ve then passed the list of actions that we want in the router.

Of course, we need to add a route for that in the conf/routes file:

GET     /assets/javascripts/routes          controllers.Application.javascriptRoutes()

Now before we implement the client side code, we need to source all the javascript dependencies that we’re going to need in the app/views/main.scala.html:

<script type="text/javascript" src="@routes.Assets.at("javascripts/jquery-1.7.1.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/jquery-play-1.7.1.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/underscore-min.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/backbone-min.js")"></script>
<script type="text/javascript" src="@routes.Assets.at("javascripts/main.js")"></script>
<script type="text/javascript" src="@routes.Application.javascriptRoutes()"></script>

§CoffeeScript

We are going to implement the client side code using CoffeeScript. CoffeeScript enhances the Javascript syntax, it is translated to Javascript and is fully interoperable with it. For example we can use our Javascript router from it. We could use Javascript, but since Play comes built in with a CoffeeScript compiler we thought we’d show that off instead!

When we added the Javascript dependencies to main.scala.html we added a dependency on main.js. This doesn’t exist yet as it is going to be the artifact that will be produced from the CoffeeScript translation. Let’s implement the coffeescript now. Open app/assets/javascripts/main.coffee:

$(".options dt, .users dt").live "click", (e) ->
    e.preventDefault()
    if $(e.target).parent().hasClass("opened")
        $(e.target).parent().removeClass("opened")
    else
        $(e.target).parent().addClass("opened")
        $(document).one "click", ->
            $(e.target).parent().removeClass("opened")
    false

$.fn.editInPlace = (method, options...) ->
    this.each ->
        methods = 
            # public methods
            init: (options) ->
                valid = (e) =>
                    newValue = @input.val()
                    options.onChange.call(options.context, newValue)
                cancel = (e) =>
                    @el.show()
                    @input.hide()
                @el = $(this).dblclick(methods.edit)
                @input = $("<input type='text' />")
                    .insertBefore(@el)
                    .keyup (e) ->
                        switch(e.keyCode)
                            # Enter key
                            when 13 then $(this).blur()
                            # Escape key
                            when 27 then cancel(e)
                    .blur(valid)
                    .hide()
            edit: ->
                @input
                    .val(@el.text())
                    .show()
                    .focus()
                    .select()
                @el.hide()
            close: (newName) ->
                @el.text(newName).show()
                @input.hide()
        # jQuery approach: http://docs.jquery.com/Plugins/Authoring
        if ( methods[method] )
            return methods[ method ].apply(this, options)
        else if (typeof method == 'object')
            return methods.init.call(this, method)
        else
            $.error("Method " + method + " does not exist.")

Now the code that you see above may be a little overwhelming to you. The first block of code activates all the option icons in the page and it is regular jquery. The second is an extension to jquery that we’ll use a bit later. The extension transforms a span into one that can be edited in place. These are just some utility methods that we are going to need to help with writing the rest of the logic.

Let’s start to write our Backbone views:

class Drawer extends Backbone.View

$ -> 
    drawer = new Drawer el: $("#projects")

We’ve now bound our drawer to a Backbone view called Drawer which has an id of projects. However we haven’t done anything useful yet. In the initialize function of the drawer, let’s bind all the groups to a new Group class and all the projects in each group to a new Project class:

class Drawer extends Backbone.View
    initialize: ->
        @el.children("li").each (i,group) ->
            new Group
                el: $(group)
            $("li",group).each (i,project) ->
                new Project
                    el: $(project)

class Group extends Backbone.View

class Project extends Backbone.View

Now we’ll add some behavior to the groups. Let’s first add a toggle function to the group so that we can hide and display the group:

class Group extends Backbone.View
    events:
        "click    .toggle"          : "toggle"
    toggle: (e) ->
        e.preventDefault()
        @el.toggleClass("closed")
        false

Earlier when we created our groups template we added some buttons including a new project button. Let’s bind a click event to that:

class Group extends Backbone.View
    events:
        "click    .toggle"          : "toggle"
        "click    .newProject"      : "newProject"
    newProject: (e) ->
        @el.removeClass("closed")
        r = jsRoutes.controllers.Projects.add()
        $.ajax
            url: r.url
            type: r.type
            context: this
            data:
                group: @el.attr("data-group")
            success: (tpl) ->
                _list = $("ul",@el)
                _view = new Project
                    el: $(tpl).appendTo(_list)
                _view.el.find(".name").editInPlace("edit")
            error: (err) ->
                $.error("Error: " + err)

Now you can see that are are using the jsRoutes Javascript router that we created before. It almost looks like we are just making an ordinary call to the Projects.add action. Invoking this actually returns an object that gives us a url and type (method) for making AJAX requests.

You don’t have to use jQuery with the Javascript router. The router is simply resolving urls and types (methods) for you.

Now if you refresh the page you should be able to create a new project. However, the new projects name is “New Project” which is not really what we want. Let’s implement the functionality to rename it:

class Project extends Backbone.View
    initialize: ->
        @id = @el.attr("data-project")
        @name = $(".name", @el).editInPlace
            context: this
            onChange: @renameProject
    renameProject: (name) ->
        @loading(true)
        r = jsRoutes.controllers.Projects.rename(@id)
        $.ajax
            url: r.url
            type: r.type
            context: this
            data:
                name: name
            success: (data) ->
                @loading(false)
                @name.editInPlace("close", data)
            error: (err) ->
                @loading(false)
                $.error("Error: " + err)
    loading: (display) ->
        if (display)
            @el.children(".delete").hide()
            @el.children(".loader").show()
        else
            @el.children(".delete").show()
            @el.children(".loader").hide()

First we’ve declared the name of our project to be editable in place, using the helper function we added earlier, and passing the renameProject() method as the callback. In our renameProject() method we’ve again used the Javascript router, this time passing a parameter, the id of the project that we are to rename. Try it out now to see if you can rename a project by double clicking on the project.

The last thing we want to implement for projects is the remove method, binding to the remove button. Add the following CoffeeScript to the Project backbone class:

    events:
        "click      .delete"        : "deleteProject"
    deleteProject: (e) ->
        e.preventDefault()
        @loading(true)
        r = jsRoutes.controllers.Projects.delete(@id)
        $.ajax
            url: r.url
            type: r.type
            context: this
            success: ->
                @el.remove()
                @loading(false)
            error: (err) ->
                @loading(false)
                $.error("Error: " + err)
        false

Once again we are using the Javascript router to invoke the delete method on the Projects controller.

As a final task we’ll add a new group button to the main template and implement the logic for it. So let’s add a new group button to the app/views/main.scala.html template, just before the closing </nav> tag:

    </ul>
    <button id="newGroup">New group</button>
</nav>

Now add an addGroup method to the Drawer class and some code to the initialize method that binds clicking the newGroup button to it:

class Drawer extends Backbone.View
    initialize: ->
        ...
        $("#newGroup").click @addGroup
    addGroup: ->
        r = jsRoutes.controllers.Projects.addGroup()
        $.ajax
            url: r.url
            type: r.type
            success: (data) ->
                _view = new Group
                    el: $(data).appendTo("#projects")
                _view.el.find(".groupName").editInPlace("edit")

Try it out. You can now create a new group.

§Testing our client side code

Although we’ve tested that things are working manually, Javascript apps can be quite fragile and easy to break in future. Play provides a very simple mechanism for testing client side code using FluentLenium. FluentLenium provides a simple way to represent your pages and the components on them in a way that is reusable and let’s you interact with them and make assertions on them.

§Implementing page objects

Let’s start by creating a page that represents our login page. Open test/pages/Login.java:

package pages;

import org.fluentlenium.core.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;

import components.*;
import controllers.*;

public class Login extends FluentPage {
    public String getUrl() {
        return routes.Application.login().url();
    }

    public void isAt() {
        assertThat(find("h1", withText("Sign in"))).hasSize(1);
    }

    public void login(String email, String password) {
        fill("input").with(email, password);
        click("button");
    }
}

You can see three methods here. Firstly, we’ve declared the URL of our page conveniently using the reverse router to get this. Then we’ve implemented an isAt() method that runs some assertions on the page to make sure that we are at this page. FluentLenium will use this when we go to the page to make sure everything is as expected. We’ve written a simple assertion here to ensure that the heading is the login page heading. Finally, we’ve implemented an action on the page which fills the login form with the users email and password and then clicks the login button.

You can read more about FluentLenium and the APIs it provides here. We won’t go into any more details in this tutorial.

Now that we can log in, let’s create a page that represents the dashboard in test/pages/Dashboard.java:

package pages;

import org.fluentlenium.core.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;

import components.*;

public class Dashboard extends FluentPage {
    public String getUrl() {
        return "/";
    }

    public void isAt() {
        assertThat(find("h1", withText("Dashboard"))).hasSize(1);
    }

    public Drawer drawer() {
        return Drawer.from(this);
    }
}

It is similarly simple, like the login page. Eventually we will add more functionality to this page but for now, since we’re only testing the drawer, we just provide a method to get the drawer. Let’s see how implementation of the drawer, in test/components/Drawer.java:

ackage components;

import java.util.*;

import org.fluentlenium.core.*;
import org.fluentlenium.core.domain.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;

public class Drawer {

    private final FluentWebElement element;

    public Drawer(FluentWebElement element) {
        this.element = element;
    }

    public static Drawer from(Fluent fluent) {
        return new Drawer(fluent.findFirst("nav"));
    }

    public DrawerGroup group(String name) {
        return new DrawerGroup(element.findFirst("#projects > li[data-group=" + name + "]"));
    }
}

Since our drawer is not a page, but rather is a component of a page, we haven’t extended FluentPage this time. We are simply wrapping a FluentWebElement and this is the <nav> element that the drawer lives in. We’ve provided a method to look up a group by name. Let’s see the implementation of the group now, so open test/components/DrawerGroup.java:

package components;

import org.fluentlenium.core.*;
import org.fluentlenium.core.domain.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;
import java.util.*;
import com.google.common.base.Predicate;

public class DrawerGroup {
    private final FluentWebElement element;

    public DrawerGroup(FluentWebElement element) {
        this.element = element;
    }

    public List<DrawerProject> projects() {
        List<DrawerProject> projects = new ArrayList<DrawerProject>();
        for (FluentWebElement e: (Iterable<FluentWebElement>) element.find("ul > li")) {
            projects.add(new DrawerProject(e));
        }
        return projects;
    }

    public DrawerProject project(String name) {
        for (DrawerProject p: projects()) {
            if (p.name().equals(name)) {
                return p;
            }
        }
        return null;
    }

    public Predicate hasProject(final String name) {
        return new Predicate() {
            public boolean apply(Object o) {
                return project(name) != null;
            }
        };
    }

    public void newProject() {
        element.findFirst(".newProject").click();
    }
}

As with Drawer we have a method for looking up a project by name. We’ve also provided a method for checking if a project with a particular name exists and used Predicate to capture this. Using Predicate will make it easy for us later when we tell FluentLenium to wait until certain conditions are true.

Finally, the last component of our model that we’ll build out is test/componenst/DrawerProject.java:

package components;

import org.fluentlenium.core.*;
import org.fluentlenium.core.domain.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;
import org.openqa.selenium.Keys;
import com.google.common.base.Predicate;

public class DrawerProject {
    private final FluentWebElement element;

    public DrawerProject(FluentWebElement element) {
        this.element = element;
    }

    public String name() {
        FluentWebElement a = element.findFirst("a.name");
        if (a.isDisplayed()) {
            return a.getText().trim();
        } else {
            return element.findFirst("input").getValue().trim();
        }
    }

    public void rename(String newName) {
        element.findFirst(".name").doubleClick();
        element.findFirst("input").text(newName);
        element.click();
    }

    public Predicate isInEdit() {
        return new Predicate() {
            public boolean apply(Object o) {
                return element.findFirst("input") != null && element.findFirst("input").isDisplayed();
            }
        };
    }

The DrawerProject allows us to lookup the name of the project, rename the project, and has a predicate for checking if the project name is in edit mode. So, it’s been a bit of work to get this far with our selenium tests, and we haven’t written any tests yet! The great thing is though is that all of these components and pages are going to be reusable from all of our selenium tests. When something about our markup changes we can just update these components and all the tests will still work.

§Implementing the tests

Open test/views/DrawerTest.java and add the following setup code:

package views;

import org.junit.*;

import play.test.*;
import static play.test.Helpers.*;

import org.fluentlenium.core.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.fluentlenium.FluentLeniumAssertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.*;
import com.google.common.base.*;

import pages.*;
import components.*;

public class DrawerTest extends WithBrowser {

    public Drawer drawer;

    @Before
    public void setUp() {
        start();
        Login login = browser.createPage(Login.class);
        login.go();
        login.login("[email protected]", "secret");
        drawer = browser.createPage(Dashboard.class).drawer();
    }
}

This time we’ve made our test case extend WithBrowser which gives us a mock web browser to work with. The default browser is HtmlUnit, a Java based headless browser, but you can also use other browsers such as Firefox and Chrome. In our setUp() method we’ve called start(), which starts both the browser and the server. We’ve then created a login page, navigated to it, logged in, and then we’ve created a dashboard page and retrieved the drawer. We’re now ready to write our first test case:

    @Test
    public void newProject() throws Exception {
        drawer.group("Personal").newProject();
        dashboard.await().until(drawer.group("Personal").hasProject("New project"));
        dashboard.await().until(drawer.group("Personal").project("New project").isInEdit());
    }

Here we’re testing new project creation. We get the Personal group using the methods we’ve already defined on drawer. You can see we’re using those predicates we wrote for testing if a group has a project and if it’s in edit mode (as it should be after we’ve created it). Let’s write another test, this time testing the rename functionality:

    @Test
    public void renameProject() throws Exception {
        drawer.group("Personal").project("Private").rename("Confidential");
        dashboard.await().until(Predicates.not(drawer.group("Personal").hasProject("Private")));
        dashboard.await().until(drawer.group("Personal").hasProject("Confidential"));
        dashboard.await().until(Predicates.not(drawer.group("Personal").project("Confidential").isInEdit()));
    }

We rename a project, wait for it to disappear, wait for the new one to appear, and then ensure that the new one is not in edit mode. In this tutorial we’re going to leave the client side tests there, but you can try now to implement your own tests for deleting projects and adding new groups.

So now we have a working and tested navigation drawer. We’ve seen how to implement a few more actions, how the Javascript router works, how to use CoffeeScript in our Play application and how to use the Javascript router from our CoffeeScript code. We’ve also seen how we can use the page object pattern to write tests for a client side code running in a headless browser. There are a few functions we haven’t implemented yet including renaming a group and deleting a group. You could try implementing them on your own or check the code in the ZenTasks sample app to see how it’s done.

Commit your work to git.

Go to the next part