JavaScriptMVC (JMVC) is an open-source jQuery-based JavaScript framework. It is nearly a comprehensive (holistic) front-end development framework; packaging utilities for testing, dependency management, documentation, and a host of useful jQuery plugins. Yet, every part of JavaScriptMVC can be used without every other part, making the library lightweight. It's Class, Model, View, and Controller combined are only 7k minified and compressed, yet even they can be used independently. JavaScriptMVC's independence lets you start small and scale to meet the challenges of the most complex applications on the web.
This chapter only covers JavaScriptMVC's
-
$.Class- a JavaScript based class system. -
$.Model- a traditional model layter -
$.View- a client side template system -
$.Controller- a jQuery plugin system that can serve as traditional controller or view.
We'll see how
JavaScriptMVC can be used as a single download that includes its entire toolset. But this chapter covers only JavaScriptMVC's MVC parts. Instead of downloading the entire framework, go to the download builder, check Controller, Model, and View's EJS templates and click download.
The download will come with minified and unminified versions of jQuery and the plugins you selected. Load these with script tags in your page:
<script type='text/javascript' src='jquery-1.6.1.js'></script>
<script type='text/javascript' src='jquerymx-1.0.custom.js'></script>
JMVC's Controller and Model are based on it's Class helper - $.Class. To create a class, call $.Class(NAME, [classProperties, ] instanceProperties]).
$.Class("Animal",{
breathe : function(){
console.log('breathe');
}
});
In the example above, instances of Animal have a breath() method. We can create a new Animal instance and call breathe() on it like:
var man = new Animal();
man.breathe();
If you want to create a sub-class, simply call the the base class with the sub-classes name and properties:
Animal("Dog",{
wag : function(){
console.log('wag');
}
})
var dog = new Dog;
dog.wag();
dog.breathe();
When a new class is created, it calls the class's init method with the arguments passed to the constructor function:
$.Class('Person',{
init : function(name){
this.name = name;
},
speak : function(){
return "I am "+this.name+".";
}
});
var justin = new Person("Justin");
assertEqual( justin.speak() , 'I am Justin.' );
Call base methods with this._super. The following overwrites person
to provide a more 'classy' greating:
Person("ClassyPerson", {
speak : function(){
return "Salutations, "+this._super();
}
});
var fancypants = new ClassyPerson("Mr. Fancy");
assertEquals( fancypants.speak() , 'Salutations, I am Mr. Fancy.')
Class's callback method return a function that has 'this' set appropriately (similar to proxy). The following creates a clicky class that counts how many times it was clicked:
$.Class("Clicky",{
init : function(){
this.clickCount = 0;
},
clicked: function(){
this.clickCount++;
},
listen: function(el){
el.click( this.callback('clicked') );
}
})
var clicky = new Clicky();
clicky.listen( $('#foo') );
clicky.listen( $('#bar') ) ;
Class lets you define inheritable static properties and methods. The following allows us to retrieve a person instance from the server by calling Person.findOne(ID, success(person) ). Success is called back with an instance of Person and has the speak method.
$.Class("Person",{
findOne : function(id, success){
$.get('/person/'+id, function(attrs){
success( new Person( attrs ) );
},'json')
}
},{
init : function(attrs){
$.extend(this, attrs)
},
speak : function(){
return "I am "+this.name+".";
}
})
Person.findOne(5, function(person){
alert( person.speak() );
})
Class provides namespacing and access to the name of the class and namespace object:
$.Class("Jupiter.Person");
Jupiter.Person.shortName; //-> 'Person'
Jupiter.Person.fullName; //-> 'Jupiter.Person'
Jupiter.Person.namespace; //-> Jupiter
Putting it all together, we can make a basic ORM-style model layer. Just by inheriting from Model, we can request data from REST services and get it back wrapped in instances of the inheriting Model.
$.Class("Model",{
findOne : function(id, success){
$.get('/'+this.fullName.toLowerCase()+'/'+id,
this.callback(function(attrs){
success( new this( attrs ) );
})
},'json')
}
},{
init : function(attrs){
$.extend(this, attrs)
}
})
Model("Person",{
speak : function(){
return "I am "+this.name+".";
}
});
Person.findOne(5, function(person){
alert( person.speak() );
});
Model("Task")
Task.findOne(7,function(task){
alert(task.name);
})
This is similar to how JavaScriptMVC's model layer works.
JavaScriptMVC's model and it associated plugins provide lots of tools around organizing model data such as validations, associations, lists and more. But the core functionality is centered around service encapsulation, type conversion, and events.
Of absolute importance to a model layer is the ability to get and set properties on the modeled data and listen for changes on a model instance. This is the Observer pattern and lies at the heart of the MVC approach - views listen to changes in the model.
Fortunately, JavaScriptMVC makes it easy to make any data observable. A great example is pagination. It's very common that multiple pagination controls exist on the page. For example, one control might provide next and previous page buttons. Another control might detail the items the current page is viewing (ex "Showing items 1-20"). All pagination controls need the exact same data:
- offset - the index of the first item to display
- limit - the number of items to display
- count - the total number of items
We can model this data with JavaScriptMVC like:
var paginate = new Model({
offset: 0,
limit: 20,
count: 200
});
The paginate variable is now observable. We can pass it to pagination controls that can read from, write to, and listen for property changes. You can read properties like normal or using the model.attr(NAME) method:
assertEqual( paginate.offset, 0 );
assertEqual( paginate.attr('limit') , 20 );
If we clicked the next button, we need to increment the offset. Change property values with model.attr(NAME, VALUE). The following moves the offset to the next page:
paginate.attr('offset',20);
When paginate's state is changed by one control, the other controls need to be notified. You can bind to a specific attribute changs with model.bind(ATTR, success( ev, newVal ) and update the control:
paginate.bind('offset', function(ev, newVal){
$('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})
You can also listen to any attribute change by binding to the 'updated.attr' event:
paginate.bind('updated.attr', function(ev, newVal){
$('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})
Tthe following is a next-previous jQuery plugin that accepts paginate data:
$.fn.nextPrev = function(paginate){
this.delegate('.next','click', function(){
var nextOffset = paginate.offset+paginate.limit;
if( nextOffset < paginate.count){
paginate.attr('offset', nextOffset );
}
})
this.delegate('.prev','click', function(){
var nextOffset = paginate.offset-paginate.limit;
if( 0 < paginate.offset ){
paginate.attr('offset', Math.max(0, nextOffset) );
}
});
var self = this;
paginate.bind('updated.attr', function(){
var next = self.find('.next'),
prev = self.find('.prev');
if( this.offset == 0 ){
prev.removeClass('enabled');
} else {
prev.removeClass('disabled');
}
if( this.offset > this.count - this.limit ){
next.removeClass('enabled');
} else {
next.removeClass('disabled');
}
})
};
There are a few problems with this plugin. First, the logic protecting a negative offset or offset above the count is done in the plugin. This logic should be done in the model. Second, if the control is removed from the page, it is not unbinding itself from paginate. We'll address the first problem in the following sections and address the second problem when we discuss controllers.
To fix the validation problem, we'll need to add additional constraints to limit what values limit, offset, and count can be. We'll need to create a pagination class.
JavaScriptMVC's model inherits from $.Class. Thus, you create a model class by inheriting from $.Model(NAME, [STATIC,] PROTOTYPE):
$.Model('Paginate',{
classProperty: 'foo'
},{
staticProperty: 'bar'
})
There are a few ways to make the Paginate model more useful. First, by adding setter methods, we can limit what values count and offset can be set to.
Settter methods are model prototype methods that are named setNAME. They get called with the val passed to model.attr(NAME, val) and a success and error callback. Typically, the method should return the value that should be set on the model instance or call error with an error message. Success is used for asynchronous setters.
The following paginate model uses setters to prevent negative counts and offsets and prevent the offset from exceeding the count.
$.Model('Paginate',{
setCount : function(newCount, success, error){
return newCount < 0 ? 0 : newCount;
},
setOffset : function(newOffset, success, error){
return newOffset < 0 ? 0 : Math.min(newOffset, !isNaN(this.count - 1) ? this.count : Infinity )
}
})
Now the nextPrev plugin can set offset with reckless abandon:
this.delegate('.next','click', function(){
paginate.attr('offset', paginate.offset+paginate.limit);
})
this.delegate('.prev','click', function(){
paginate.attr('offset', paginate.offset-paginate.limit );
});
We can add default values to Paginate instances by setting the static defaults property object that contains the default attributes. When a new paginate instance is created, if no value is provided, it will use the default value.
$.Model('Paginate',{
defaults : {
count: Infinity,
offset: 0,
limit: 100
}
},{
setCount : function(newCount, success, error){ ... },
setOffset : function(newOffset, success, error){ ... }
})
var paginate = new Paginate();
assertEqual(paginate.limit, 100);
This is getting sexu, but the Paginate model can make it event easier to move to the next and previous page and know if it's possible by adding helper methods.
Helper methods are prototype methods that help set or get useful data on model instances. The following, completed, Paginate model includes a next and prev method that will move to the next and previous page if possible. It also provides a canNext and canPrev method that returns if the instance can move to the next page or not.
$.Model('Paginate',{
defaults : {
count: Infinity,
offset: 0,
limit: 100
}
},{
setCount : function( newCount ){
return Math.max(0, newCount );
},
setOffset : function( newOffset ){
return Math.max( 0 , Math.min(newOffset, this.count ) )
},
next : function(){
this.attr('offset', this.offset+this.limit);
},
prev : function(){
this.attr('offset', this.offset - this.limit )
},
canNext : function(){
return this.offset > this.count - this.limit
},
canPrev : function(){
return this.offset > 0
}
})
Thus jQuery widget becomes much more refined:
$.fn.nextPrev = function(paginate){
this.delegate('.next','click', function(){
paginate.attr('offset', paginate.offset+paginate.limit);
})
this.delegate('.prev','click', function(){
paginate.attr('offset', paginate.offset-paginate.limit );
});
var self = this;
paginate.bind('updated.attr', function(){
self.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled')
self.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled');
})
};
We've just seen how $.Model is useful for modeling client side state. However, for most applications, the critical data on the server, not on the client. The client needs to create, retrieve, update and delete (CRUD) data on the server. Maintaining the duality of data on the client and server is tricky business. $.Model is used to simplify this problem.
$.Model is extremely flexible. It can be made to work with all sorts of services types and data types. This book covers only how $.Model works with the most common and popular type of service, Representational State Transfer (REST), and data type, JSON.
A REST service uses urls and the HTTP verbs POST, GET, PUT, DELETE to create, retrieve, update, and delete data respectively. For example, take a tasks service that allowed you to create, retrieve, update and delete tasks might look like:
ACTION VERB URL BODY RESPONSE
Create a task POST /tasks name=do the dishes {
"id" : 2,
"name" : "do the dishes",
"acl" : "rw" ,
"createdAt": 1303087131164 // April 17 2011
}
Get a task GET /task/2 {
"id" : 2,
"name" : "do the dishes",
"acl" : "rw" ,
"createdAt": 1303087131164 // April 17 2011
}
Get tasks GET /tasks
[{
"id" : 1,
"name" : "take out trash",
"acl" : "r",
"createdAt": 1303000731164 // April 16 2011
},
{
"id" : 2,
"name" : "do the dishes",
"acl" : "rw" ,
"createdAt": 1303087131164 // April 17 2011
}]
Update a task PUT /task/2 name=take out recycling {
"id" : 2,
"name" : "take out recycling",
"acl" : "rw" ,
"createdAt": 1303087131164 // April 17 2011
}
Delete a task DELETE /task/2 {}
TODO: We can label the urls
The following connects to task services, letting us create, retrieve, update and delete tasks from the server:
$.Model("Task",{
create : "POST /tasks.json",
findOne : "GET /tasks/{id}.json",
findAll : "GET /tasks.json",
update : "PUT /tasks/{id}.json",
destroy : "DELETE /tasks/{id}.json"
},{ });
The following table details how to use the task model to CRUD tasks.
ACTION CODE DESCRIPTION
Create a task
new Task({ name: 'do the dishes'}).save(
success( task, data ),
error( jqXHR)
) -> taskDeferred
To create an instance of a model on the server, first create an instance with new Model(attributes). Then call save().
Save checks if the task has an id. In this case it does not so save makes a create request with the tasks attributes. Save takes two parameters:
success - a function that gets called if the save is successful. Success gets called with the task instance and the data returned by the server.
error - a function that gets called if there is an error with the request. It gets called with jQuery's wrapped XHR object.
Save returns a deferred that resolves to the created task.
Get a task
Task.findOne(params,
success( task ),
error( jqXHR)
) -> taskDeferred
Retrieves a single task from the server. It takes three parameters:
params - data to pass to the server. Typically an id like: {id: 2}.
success - a function that gets called if the request is succesful. Success gets called with the task instance.
error - a function that gets called if there is an error with the request. It gets called with jQuery's wrapped XHR object.
findOne returns a deferred that resolves to the task.
Get tasks
Task.findAll(params,
success( tasks ),
error( jqXHR)
) -> tasksDeferred
Retrieves an array of tasks from the server. It takes three parameters:
params - data to pass to the server. Typically, it's an empty object ({}) or filters: {limit: 20, offset: 100}.
success - a function that gets called if the request is succesful. Success gets called with an array of task instances. It can also get called with a $.Model.List of task instances.
error - a function that gets called if there is an error with the request. It gets called with jQuery's wrapped XHR object.
findOne returns a deferred that resolves to an array of tasks.
Update a task
task.attr('name','take out recycling');
task.save(
success( task, data ),
error( jqXHR)
) -> taskDeferred
To update the server, first change the attributes of a model instance with attr. Then call save().
Save takes the same arguments and returns the same deferred as the create task case.
Destroy a task
task.destroy(
success( task, data ),
error( jqXHR)
) -> taskDeferred
Destroys a task on the server. Destroy takes two parameters:
success - a function that gets called if the save is successful. Success gets called with the task instance and the data returned by the server.
error - a function that gets called if there is an error with the request. It gets called with jQuery's wrapped XHR object.
Destroy returns a deferred that resolves to the destroyed task.
The Task model has essentially become a contract to our services!
Did you notice how the server responded with createdAt values as numbers like 1303173531164. This number is actually April 18th, 2011. Instead of getting a number back from task.createAt, it would be much more useful if it returns a JavaScript date created with new Date(1303173531164). We could do this with a setCreatedAt setter. But, if we have lots of date types, this will quickly get repetitive.
To make this easy, $.Model lets you define the type of an attribute and a converter function for those types. Set the type of attributes on the static attributes object and converter methods on the static convert object.
$.Model('Task',{
attributes : {
createdAt : 'date'
},
convert : {
date : function(date){
return typeof date == 'number' ? new Date(date) : date;
}
}
},{});
Task now converts createdAt to a Date type. To list the year of each task, write:
Task.findAll({}, function(tasks){
$.each(tasks, function(){
console.log( "Year = "+this.createdAt.fullYear() )
})
});
Model publishes events when an instance has been created, updated, or destroyed. You can listen to these events globally on the Model or on an individual model instance. Use MODEL.bind(EVENT, callback( ev, instance ) ) to listen for created, updated, or destroyed events.
Lets say we wanted to know when a task is created and add it to the page. And after it's been added to the page, we'll listen for updates on that task to make sure we are showing its name correctly. We can do that like:
Task.bind('created', function(ev, task){
var el = $('<li>').html(todo.name);
el.appendTo($('#todos'));
task.bind('updated', function(){
el.html(this.name)
}).bind('destroyed', function(){
el.remove()
})
})
JavaScriptMVC's views are really just client side templates. Client side templates take data and return a string. Typically, the strings are HTML intended to be inserted into the DOM.
jQuery.View is a templating interface that takes care of complexities using templates:
- Convenient and uniform syntax
- Template loading from html elements and external files.
- Synchronous and asynchronous template loading.
- Template preloading.
- Caching of processed templates.
- Bundling of processed templates in production builds.
JavaScriptMVC comes pre-packaged with 4 different template engines:
- EJS
- JAML
- Micro
- Tmpl
This tutorial uses EJS templates, but all the following techniques will work with any template engine with minor syntax difference.
When using views, you almost always want to insert the results of a rendered template into the page. jQuery.View overwrites the jQuery modifiers so using a view is as easy as:
$("#foo").html('mytemplate.ejs',{message: 'hello world'})
This code:
-
Loads the template in file 'mytemplate.ejs'. It might look like:
<h2><%= message %></h2>
-
Renders it with {message: 'hello world'}, resulting in:
<h2>hello world</h2>
-
Inserts the result into the foo element. Foo might look like:
<div id='foo'><h2>hello world</h2></div>
You can use a template with the following jQuery modifier methods:
$('#bar').after('temp.ejs',{});
$('#bar').append('temp.ejs',{});
$('#bar').before('temp.ejs',{});
$('#bar').html('temp.ejs',{});
$('#bar').prepend('temp.ejs',{});
$('#bar').replaceWidth('temp.ejs',{});
$('#bar').text('temp.ejs',{});
View can load from script tags or from files. To load from a script tag, create a script tag with your template and an id like:
<script type='text/ejs' id='recipesEJS'>
<% for(var i=0; i < recipes.length; i++){ %>
<li><%=recipes[i].name %></li>
<%} %>
</script>
Render with this template like:
$("#foo").html('recipesEJS',recipeData)
Notice we passed the id of the element we want to render.
StealJS is JavaScriptMVC's dependency management and build system. Although not covered in this book, it's worth noting that StealJS is able to build templates into a production file. This makes the application load much faster as it doesn't have to wait for templates to load.
By default, retrieving requests is done synchronously. This is fine because StealJS packages view templates with your JS download.
However, some people might not be using StealJS or want to delay loading templates until necessary. If you have the need, you can provide a callback paramter like:
$("#foo").html('recipes',recipeData, function(result){
this.fadeIn()
});
The callback function will be called with the result of the rendered template and 'this' will be set to the original jQuery object.
JavaScriptMVC's controllers are really a jQuery plugin factory. They can be used as a traditional view, for example, making a slider widget, or a traditional controller, creating view-controllers and binding them to models. If anything happens in your application, a controller should respond to it.
Controllers are the most powerful piece of JavaScriptMVC and go the furthest towards helping you develop better JavaScript applications, with features like:
- jQuery helper
- auto bind / unbind
- parameterized actions
- defaults
- pub / sub
- automatic determinism
Controllers inherit from $.Class. Each Controller is instantiated on an HTML element. Controller methods, or actions, use event delegation for listen to events inside the parent element.
$.Controller('Tabs',{
click: function() {...},
'.tab click' : function() {...},
'.delete click' : function() {...}
})
$('#tabs').tabs();
When a controller class is created, it automatically creates a jQuery.fn method of a similar name. These methods have three naming rules:
-
Everything is lowercased
-
"." is replaced with "_"
-
"Controllers" is removed from the name
$.Controller("Foo.Controllers.Bar") // -> .foo_bar()
You create a controller by calling this jQuery.fn method on any jQuery collection. You can pass in options, which are used to set this.options in the controller.
$(".thing").my_widget({message : "Hello"})
Controllers provide automatic determinism for your widgets. This means you can look at a controller and know where in the DOM they operate, and vice versa.
First, when a controller is created, it adds its underscored name as a class name on the parent element.
<div id='historytab' class='history_tabs'></div>
You can look through the DOM, see a class name, and go find the corresponding controller.
Second, the controller saves a reference to the parent element in this.element. On the other side, the element saves a reference to the controller instance in jQuery.data.
$("#foo").data('controllers')
A helper method called controller (or controllers) using the jQuery.data reference to quickly look up controller instance on any element.
$("#foo").controller() // returns first controller found
$("#foo").controllers() // returns an array of all controllers on this element
Finally, actions are self labeling, meaning if you look at a method called ".foo click", there is no ambiguity about what is going on in that method.
If you name an event with the pattern "selector action", controllers will set these methods up as event handlers with event delegation. Even better, these event handlers will automatically be removed when the controller is destroyed.
".todo mouseover" : function( el, ev ) {}
The el passed as the first argument is the target of the event, and ev is the jQuery event. Each handler is called with "this" set to the controller instance, which you can use to save state.
Part of the magic of controllers is their automatic removal and cleanup. Controllers bind to the special destroy event, which is triggered whenever an element is removed via jQuery. So if you remove an element that contains a controller with el.remove() or a similar method, the controller will remove itself also. All events bound in the controller will automatically clean themselves up.
Controllers can be given a set of default options. Users creating a controller pass in a set of options, which will overwrite the defaults if provided.
In this example, a default message is provided, but can is overridden in the second example by "hi".
$.Controller("Message", {
defaults : {
message : "Hello World"
}
},{
init : function(){
this.element.text(this.options.message);
}
})
$("#el1").message(); //writes "Hello World"
$("#el12").message({message: "hi"}); //writes "hi"
Controllers provide the ability to set either the selector or action of any event via a customizable option. This makes controllers potentially very flexible. You can create more general purpose event handlers and instantiate them for different situations.
The following listens to li click for the controller on #clickMe, and "div mouseenter" for the controller on #touchMe.
$.Controller("Hello", {
defaults: {item: “li”, helloEvent: “click”}
}, {
“{item} {helloEvent}" : function(el, ev){
alert('hello')� el // li, div
}
})
$("#clickMe").hello({item: “li”, helloEvent : "click"});
$("#touchMe").hello({item: “div”, helloEvent : "mouseenter"});
JavaScriptMVC applications often use OpenAjax event publish and subscribe as a good way to globally notify other application components of some interesting event. The jquery/controller/subscribe method lets you subscribe to (or publish) OpenAjax.hub messages:
$.Controller("Listener",{
"something.updated subscribe" : function(called, data){}
})
// called elsewhere
this.publish("some.event", data);
Controllers provide support for many types of special events. Any event that is added to jQuery.event.special and supports bubbling can be listened for in the same way as a DOM event like click.
$.Controller("MyHistory",{
"history.pagename subscribe" : function(called, data){
//called when hash = #pagename
}
})
Drag, drop, hover, and history and some of the more widely used controller events. These events will be discussed later.
Testing is an often overlooked part of front end development. Most functional testing solutions are hard to set up, expensive, use a difficult (non JavaScript) API, are too hard to debug, and are don't accurately simulate events. FuncUnit, JavaScriptMVC's testing solution, is designed to solve all these problems. No setup, firebug debugging, a jQuery-like API, and the most accurate possible event simulation make FuncUnit a comprehensive testing solution.
FuncUnit is a collection of several components:
- jQuery - for querying elements and testing their conditions
- QUnit - jQuery's unit test framework for setting up tests
- Syn - an event simulation library made for FuncUnit that simulates clicks, types, drags
- Selenium - used to programatically open and close browsers
FuncUnit tests are written in JavaScript, with an API that looks identical to jQuery. To run them, you open a web page that loads your test. It opens your application in another page, runs your test, and shows the results.
Alternatively, the same test will run from the command line, via Selenium. You run a command (or automate this in your continuous integration system), which launches a browser, runs your tests, and reports results.
Getting started with writing a FuncUnit test involves:
-
Creating a funcunit.html page that loads a test script. The tests run within the QUnit framework, so the page needs some elements required by QUnit.
<head>
<link rel="stylesheet" type="text/css" href="funcunit/qunit/qunit.css" />
<title>FuncUnit Test</title>
</head>
<body>
<h1 id="qunit-header">funcunit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<script type='text/javascript' src='steal/steal.js?steal[app]=myapp/test/funcunit/mytest.js'></script>
</body>
-
Create the test script. Steal funcunit, use QUnit's setup method to set your test up, and the test method to write your test.
steal.plugins('funcunit').then(function(){
module("yourapp test", {
setup: function(){
S.open("//path/to/your/page.html");
}
});
test("First Test", function(){
ok(true, "test passed");
});
})
You should now have a working basic FuncUnit test. Open the page and see it run.
Writing FuncUnit tests involves a similar repeated pattern:
- Opening a page (only do this once in the setup method).
- Perform some action (click a link, type into an input, drag an element)
- Wait for some condition to be true (an element becomes visible, an element's height reaches a certain value)
3*. Check conditions of your page (does the offset of a menu element equal some expected value).
- This can be done implicitly in step 2. If the conditions of your wait don't become true, the test will never complete and will fail.
This pattern breaks down into the three types of methods in the FuncUnit API: actions, waits, and getters.
Most commands (except open) follow the pattern of being called on a FuncUnit object, similar to a jQuery.fn method. The S method is similar to $, except it doesn't return a jQuery object. It accepts any valid jQuery selector and the results are chainable:
S("a.myel").click().type("one").visible();
If you're familiar with QUnit, this section will be review. FuncUnit adds its own API on top of the QUnit framework.
Module is the method used to define a collection of tests. It accepts a module name, and an object containing a setup method (called before each test), and a teardown method (called after each test).
module("module name", {
setup: function(){}
teardown: function(){}
});
Test is the method used to define each test. Ok and equals are the two most common assertions. If their conditions are true, the assertion will pass. If any assertion fails, the test fails.
Actions include open, click, dblclick, rightclick, type, move, drag, and scroll.
S("#foo").click();
S("#foo").type("one");
S("#foo").drag("#bar");
They use the syn library to accurately simulate the event exactly as the browser would process it from a real user. Click causes the correct sequence of browser events in each browser (mousedown, click, mouseup, focus if its a form element).
Note: Action commands don't actually get processed synchronously. This means you can't set a breakpoint on an action and step through the statements one by one. Each action command is asynchronous, so in order to prevent crazy nested callbacks everywhere, the FuncUnit API will add action commands to a queue, calling each after the previously queued item has completed.
Waits are used to make your tests wait for some condition in the page to be true before proceeding. Waits are needed after almost every action to prevent your tests from being brittle. If you don't use a wait, you're assuming the action's results are instantaneous, which often, they aren't.
Waits correspond to jQuery methods of the same name.
Dimensions - width, height
Attributes - attr, hasClass, val, text, html
Position - position, offset, scrollLeft, scrollTop
Selector - size, exists, missing
Style - css, visible, invisible
Waits, like actions, are asynchronous commands, and add themselves to the FuncUnit command queue, so they can't be inspected with breakpoints.
Each wait accepts the attribute you'd expect it to to wait for its condition to be true. For example, width accepts a single number (the width to wait for). HasClass accepts a class string. Css accepts a property and its value.
You can also pass a function instead of a wait value. This function will be called over and over until it is true, and then the wait is complete.
S(".foo").width(10); // wait for foo to be 10px
S(".foo").hasClass("bar") // wait for foo to have class "bar"
S(".foo").visible() // wait for foo to be visible
Getters are used to test conditions of the page, usually within an assertion. They are actually the exact same method names as waits, listed above, but called without a wait condition.
var width = S(".foo").width();
var text = S(".foo").text();
Every action and wait command accepts an optional callback function, which is called after the method completes. In these callbacks, you'd test page conditions with getters and do assertions.
S(".foo").visible(function(){
ok(S(".bar").size(), 5, "there are 5 bars");
});
You can set breakpoints inside these callbacks, test page conditions, and debug assertions that are failing.
Here's a test for an autocomplete widget.
module("autosuggest",{
setup: function() {
S.open('autosuggest.html')
}
});
test("JavaScript results",function(){
S('input').click().type("JavaScript")
// wait until we have some results
S('.autocomplete_item').visible(function(){
equal( S('.autocomplete_item').size(), 5, "there are 5 results")
})
});
There are two ways to run tests: browser and command line. To run from browser, just open your test page in the browser. Devs can use this while developing and debugging.
To run from command line, open your test with funcunit/envjs.
funcunit/envjs myapp/funcunit.html
You can configure Selenium options (like browsers) in myapp/settings.js.
FuncUnit = { browsers: ["*firefox", "*iexplore", "*safari", "*googlechrome"] };
JavaScriptMVC is packed with jQuery helpers that make building a jQuery app easier and fast. Here's the some of the most useful plugins:
Rapidly retrieve multiple css styles on a single element:
$('#foo').curStyles('paddingTop',
'paddingBottom',
'marginTop',
'marginBottom');
Often you need to start building JS functionality before the server code is ready. Fixtures simulate Ajax responses. They let you make Ajax requests and get data back. Use them by mapping request from one url to another url:
$.fixture("/todos.json","/fixtures/todos.json")
And then make a request like normal:
$.get("/todos.json",{}, function(){},'json')