TobiExpressive server-side functional testing with jQuery and jsdom. | |
| lib/assertions/should.js |
Module dependencies.
|
var Assertion = require('should').Assertion
, statusCodes = require('http').STATUS_CODES
, j = function(elem){ return '[jQuery ' + i(elem.selector.replace(/^ *\* */, '')) + ']'; }
, i = require('sys').inspect;
|
Number strings.
|
var nums = { 0: 'none', 1: 'one', 2: 'two', 3: 'three' };
|
Return string representation for n .
param: Number n return: String api: private
|
function n(n) { return nums[n] || n; }
|
Assert text as str or a RegExp .
|
Assertion.prototype.text = function(str){
var elem = this.obj
, text = elem.text();
if (str instanceof RegExp) {
this.assert(
str.test(text)
, 'expected ' + j(elem)+ ' to have text matching ' + i(str)
, 'expected ' + j(elem) + ' text ' + i(text) + ' to not match ' + i(str));
} else {
this.assert(
str == text
, 'expected ' + j(elem) + ' to have text ' + i(str) + ', but has ' + i(text)
, 'expected ' + j(elem) + ' to not have text ' + i(str));
}
return this;
};
|
Assert that many child elements are present via selector .
When negated, <= 1 is a valid length.
|
Assertion.prototype.many = function(selector){
var elem = this.obj
, elems = elem.find(selector)
, len = elems.length;
this.assert(
this.negate ? len > 1 : len
, 'expected ' + j(elem) + ' to have many ' + i(selector) + ' tags, but has ' + n(len)
, 'expected ' + j(elem) + ' to not have many ' + i(selector) + ' tags, but has ' + n(len));
return this;
};
|
Assert that one child element is present via selector
with optional text assertion..
|
Assertion.prototype.one = function(selector, text){
var elem = this.obj
, elems = elem.find(selector)
, len = elems.length;
this.assert(
1 == len
, 'expected ' + j(elem) + ' to have one ' + i(selector) + ' tag, but has ' + n(len)
, 'expected ' + j(elem) + ' to not have one ' + i(selector) + ' tag, but has ' + n(len));
if (undefined != text) {
elems.should.have.text(text);
}
return this;
};
|
Assert existance attr key with optional val .
|
Assertion.prototype.attr = function(key, val){
var elem = this.obj
, attr = elem.attr(key);
if (!val || (val && !this.negate)) {
this.assert(
attr.length
, 'expected ' + j(elem) + ' to have attribute ' + i(key)
, 'expected ' + j(elem) + ' to not have attribute ' + i(key) + ', but has ' + i(attr));
}
if (val) {
this.assert(
val == attr
, 'expected ' + j(elem) + ' to have attribute ' + i(key) + ' with ' + i(val) + ', but has ' + i(attr)
, 'expected ' + j(elem) + ' to not have attribute ' + i(key) + ' with ' + i(val));
}
return this;
};
|
Assert presence of the given class name .
|
Assertion.prototype.class = function(name){
var elem = this.obj;
this.assert(
elem.hasClass(name)
, 'expected ' + j(elem) + ' to have class ' + i(name) + ', but has ' + i(elem.attr('class'))
, 'expected ' + j(elem) + ' to not have class ' + i(name));
return this;
};
|
Assert that header field has the given val .
|
Assertion.prototype.header = function(field, val){
this.obj.should.have.property('headers');
this.obj.headers.should.have.property(field.toLowerCase(), val);
return this;
};
|
Assert .statusCode of code .
|
Assertion.prototype.status = function(code){
this.obj.should.have.property('statusCode');
var status = this.obj.statusCode;
this.assert(
code == status
, 'expected response code of ' + code + ' ' + i(statusCodes[code])
+ ', but got ' + status + ' ' + i(statusCodes[status])
, 'expected to not respond with ' + code + ' ' + i(statusCodes[code]));
return this;
};
|
Assert id attribute.
|
Assertion.prototype.id = attr('id');
|
Assert title attribute.
|
Assertion.prototype.title = attr('title');
|
Assert alt attribute.
|
Assertion.prototype.alt = attr('alt');
|
Assert href attribute.
|
Assertion.prototype.href = attr('href');
|
Assert src attribute.
|
Assertion.prototype.src = attr('src');
|
Assert rel attribute.
|
Assertion.prototype.rel = attr('rel');
|
Assert media attribute.
|
Assertion.prototype.media = attr('media');
|
Assert name attribute.
|
Assertion.prototype.name = attr('name');
|
Assert action attribute.
|
Assertion.prototype.action = attr('action');
|
Assert method attribute.
|
Assertion.prototype.method = attr('method');
|
Assert value attribute.
|
Assertion.prototype.value = attr('value');
|
Assert enabled.
|
Assertion.prototype.__defineGetter__('enabled', function(){
var elem = this.obj
, disabled = elem.attr('disabled');
this.assert(
!disabled
, 'expected ' + j(elem) + ' to be enabled'
, '<not implemented, use .disabled>');
return this;
});
|
Assert disabled.
|
Assertion.prototype.__defineGetter__('disabled', function(){
var elem = this.obj
, disabled = elem.attr('disabled');
this.assert(
disabled
, 'expected ' + j(elem) + ' to be disabled'
, '<not implemented, use .enabled>');
return this;
});
|
Assert checked.
|
Assertion.prototype.__defineGetter__('checked', bool('checked'));
|
Assert selected.
|
Assertion.prototype.__defineGetter__('selected', bool('selected'));
|
Generate a boolean assertion function for the given attr name .
param: String name return: Function api: private
|
function bool(name) {
return function(){
var elem = this.obj;
this.assert(
elem.attr(name)
, 'expected ' + j(elem) + ' to be ' + name
, 'expected ' + j(elem) + ' to not be ' + name);
return this;
}
}
|
Generate an attr assertion function for the given attr name .
param: String name return: Function api: private
|
function attr(name) {
return function(expected){
var elem = this.obj
, val = elem.attr(name);
this.assert(
expected == val
, 'expected ' + j(elem) + ' to have ' + name + ' ' + i(expected) + ', but has ' + i(val)
, 'expected ' + j(elem) + ' to not have ' + name + ' ' + i(expected));
return this;
}
}
|
| lib/browser.js |
Module dependencies.
|
var EventEmitter = require('events').EventEmitter
, Cookie = require('./cookie')
, CookieJar = require('./cookie/jar')
, jsdom = require('jsdom')
, jQuery = require('../support/jquery')
, http = require('http');
|
Starting portno.
|
var port = 8888;
|
Initialize a new Browser with the given html or server .
|
var Browser = module.exports = exports = function Browser(html) {
this.history = [];
this.cookieJar = new CookieJar;
if ('string' == typeof html) {
this.parse(html);
} else {
this.server = html;
this.server.pending = 0;
this.server.port = 8888;
}
};
|
Inherit from EventEmitter.prototype .
|
Browser.prototype.__proto__ = EventEmitter.prototype;
|
Parse the given html and populate:
param: String html api: public
|
Browser.prototype.parse = function(html){
this.source = html;
this.window = jsdom.jsdom(wrap(html)).createWindow();
this.jQuery = jQuery.create(this.window);
this.jQuery.browser = this.jQuery.fn.browser = this;
require('./jquery')(this, this.jQuery);
this.context = this.jQuery('*');
};
|
Set the jQuery context for the duration of fn() to selector .
|
Browser.prototype.within =
Browser.prototype.context = function(selector, fn){
var prev = this.context;
this.context = this.context.find(selector);
fn();
this.context = prev;
return this;
};
|
Request path with method and callback fn(jQuery) .
|
Browser.prototype.request = function(method, path, options, fn, saveHistory){
var self = this
, server = this.server
, host = '127.0.0.1'
, headers = options.headers || {};
if (!server) throw new Error('no .server present');
++server.pending;
if (!server.fd) {
server.listen(++port, host);
server.client = http.createClient(port);
}
if (false !== saveHistory) this.history.push(path);
var cookies = this.cookieJar.get({ url: path });
if (cookies.length) {
headers.Cookie = cookies.map(function(cookie){
return cookie.name + '=' + cookie.value;
}).join('; ');
}
headers.Host = host;
var req = server.client.request(method, path, headers);
req.on('response', function(res){
var status = res.statusCode
, buf = '';
if (res.headers['set-cookie']) {
self.cookieJar.add(new Cookie(res.headers['set-cookie']));
}
if (status >= 200 && status < 300) {
var contentType = res.headers['content-type'];
if (~contentType.indexOf('json')) {
res.body = '';
res.on('data', function(chunk){ res.body += chunk; });
res.on('end', function(){
try {
res.body = JSON.parse(res.body);
fn(res);
} catch (err) {
self.emit('error', err);
}
});
return;
}
if (!~contentType.indexOf('text/html')) {
return fn(res);
}
res.setEncoding('utf8');
res.on('data', function(chunk){ buf += chunk; });
res.on('end', function(){
self.parse(buf);
fn(res, function(selector){
return self.context.find(selector);
});
});
} else if (status >= 300 && status < 400) {
var location = res.headers.location;
self.emit('redirect', location);
self.request('GET', location, options, fn);
} else {
var err = new Error(
method + ' ' + path
+ ' responded with '
+ status + ' "' + http.STATUS_CODES[status] + '"');
self.emit('error', err);
}
});
req.end(options.body);
return this;
};
|
GET path and callback fn(jQuery) .
|
Browser.prototype.get =
Browser.prototype.visit =
Browser.prototype.open = function(path, options, fn, saveHistory){
if ('function' == typeof options) {
saveHistory = fn;
fn = options;
options = {};
}
return this.request('GET', path, options, fn, saveHistory);
};
|
POST path and callback fn(jQuery) .
|
Browser.prototype.post = function(path, options, fn, saveHistory){
if ('function' == typeof options) {
saveHistory = fn;
fn = options;
options = {};
}
return this.request('POST', path, options, fn, saveHistory);
};
|
GET the last page visited, or the nth previous page.
|
Browser.prototype.back = function(n, fn){
if ('function' == typeof n) fn = n, n = 1;
while (n--) this.history.pop();
return this.get(this.path, fn, false);
};
|
Locate elements via the given selector and locator supporting:
- element text
- element name attribute
- css selector
param: String selector param: String locator return: jQuery api: public
|
Browser.prototype.locate = function(selector, locator){
var self = this
, $ = this.jQuery;
var elems = this.context.find(selector).filter(function(){
var elem = $(this);
return locator == elem.text()
|| locator == elem.attr('id')
|| locator == elem.attr('value')
|| locator == elem.attr('name')
|| elem.is(locator);
});
if (elems && !elems.length) throw new Error('failed to locate "' + locator + '" in context of selector "' + selector + '"');
return elems;
};
|
Return the current path.
return: String api: public
|
Browser.prototype.__defineGetter__('path', function(){
return this.history[this.history.length - 1];
});
|
Click the given locator and callback fn(res) .
|
Browser.prototype.click = function(locator, fn){
return this.jQuery(this.locate(':submit, :button, a', locator)).click(fn, locator);
};
|
Assign val to the given locator .
|
Browser.prototype.type = function(locator, val){
this.jQuery(this.locate('input, textarea', locator)).val(val);
return this;
};
|
Uncheck the checkbox with the given locator .
|
Browser.prototype.uncheck = function(locator){
this.locate(':checkbox', locator)[0].removeAttribute('checked');
return this;
};
|
Check the checkbox with the given locator .
|
Browser.prototype.check = function(locator){
this.locate(':checkbox', locator)[0].setAttribute('checked', 'checked');
return this;
};
|
Select options at locator .
|
Browser.prototype.select = function(locator, options){
this.jQuery(this.locate('select', locator)).select(options);
return this;
};
|
Submit form at the optional locator and callback fn(res) .
|
Browser.prototype.submit = function(locator, fn){
if ('function' == typeof locator) {
fn = locator;
locator = '*';
}
return this.jQuery(this.locate('form', locator)).submit(fn, locator);
};
|
Fill the given form fields and optional locator .
|
Browser.prototype.fill = function(locator, fields){
if ('object' == typeof locator) {
fields = locator;
locator = '*';
}
this.jQuery(this.locate('form', locator)).fill(fields);
return this;
};
|
Ensure html / body tags exist.
return: String api: public
|
function wrap(html) {
if (!~html.indexOf('<body')) {
html = '<body>' + html + '</body>';
}
if (!~html.indexOf('<html')) {
html = '<html>' + html + '</html>';
}
return html;
}
|
| lib/cookie/index.js |
Module dependencies.
|
var url = require('url');
|
Initialize a new Cookie with the given cookie str and req .
|
var Cookie = exports = module.exports = function Cookie(str, req) {
this.str = str;
this.name = str.substr(0, str.indexOf('='));
str.split(/ *; */).reduce(function(obj, pair){
pair = pair.split(/ *= */);
obj[pair[0]] = pair[1] || true;
return obj;
}, this);
this.value = this[this.name];
this.expires = this.expires
? new Date(this.expires)
: Infinity;
this.path = this.path
? this.path.trim()
: url.parse(req.url).pathname;
};
|
Return the original cookie string.
return: String api: public
|
Cookie.prototype.toString = function(){
return this.str;
};
|
| lib/cookie/jar.js |
Module dependencies.
|
var url = require('url');
|
Initialize a new CookieJar .
|
var CookieJar = exports = module.exports = function CookieJar() {
this.cookies = [];
};
|
Add the given cookie to the jar.
param: Cookie cookie api: public
|
CookieJar.prototype.add = function(cookie){
this.cookies.push(cookie);
};
CookieJar.prototype.get = function(req){
var path = url.parse(req.url).pathname
, now = new Date;
return this.cookies.filter(function(cookie){
return 0 == path.indexOf(cookie.path)
&& now < cookie.expires;
});
};
|
| lib/jquery/fill.js |
Select options.
|
exports.select = function($, elems, val){
elems.select(val);
};
|
Fill inputs:
- toggle radio buttons
- check checkboxes
default to .val(val)
|
exports.input = function($, elems, val){
switch (elems.attr('type')) {
case 'radio':
elems.each(function(){
var elem = $(this);
val == elem.attr('value')
? elem.attr('checked', true)
: elem.removeAttr('checked');
});
break;
case 'checkbox':
val
? elems.attr('checked', true)
: elems.removeAttr('checked');
break;
default:
elems.val(val);
}
};
|
Fill textarea.
|
exports.textarea = function($, elems, val){
elems.val(val);
}
|
| lib/jquery/index.js |
Module dependencies.
|
var parse = require('url').parse
, fill = require('./fill');
|
Augment the given jQuery instance.
param: Browser browser param: jQuery $ api: private
|
module.exports = function(browser, $){
|
Select the given options by text or value attr.
param: Array options api: public
|
$.fn.select = function(options){
if ('string' == typeof options) options = [options];
this.find('option').filter(function(i, option){
var selected = ~options.indexOf($(option).text())
|| ~options.indexOf(option.getAttribute('value'));
if (selected) {
option.setAttribute('selected', 'selected');
} else {
option.removeAttribute('selected');
}
})
return this;
};
|
Click the first element with callback fn(jQuery, res)
when text/html or fn(res) otherwise.
- requests a tag href
- requests form submit's parent form action
param: Function fn api: public
|
$.fn.click = function(fn, locator){
var url
, prop = 'element'
, method = 'get'
, locator = locator || this.selector
, options = {};
switch (this[0].nodeName) {
case 'A':
prop = 'href';
url = this.attr('href');
break;
case 'INPUT':
if ('submit' == this.attr('type')) {
var form = this.parent('form').first();
method = form.attr('method') || 'get';
url = form.attr('action') || parse($.browser.path).pathname;
if ('get' == method) {
url += '?' + form.serialize();
} else {
options.body = form.serialize();
options.headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
}
}
break;
}
if (!url) throw new Error('failed to click ' + locator + ', ' + prop + ' not present');
browser[method](url, options, fn);
return this;
};
|
Apply fill rules to the given fields .
param: Object fields api: public
|
$.fn.fill = function(fields){
for (var locator in fields) {
var val = fields[locator]
, elems = browser.locate('select, input, textarea', locator)
, name = elems[0].nodeName.toLowerCase();
fill[name]($, elems, val);
}
return this;
};
|
Submit this form with the given callback fn.
param: Function fn api: public
|
$.fn.submit = function(fn, locator){
var submit = this.find(':submit');
if (submit.length) {
submit.click(fn, locator);
} else {
$('<input id="tobi-submit" type="submit" />')
.appendTo(this)
.click(fn, locator)
.remove();
}
};
};
|
| lib/tobi.js |
Library version.
|
exports.version = '0.0.1';
|
Expose Browser .
|
exports.Browser = require('./browser');
|
Expose Cookie .
|
exports.Cookie = require('./cookie');
|
Expose CookieJar .
|
exports.CookieJar = require('./cookie/jar');
|
Initialize a new Browser .
|
exports.createBrowser = function(str){
return new exports.Browser(str);
};
|
Automated should.js support.
|
try {
require('should');
require('./assertions/should');
} catch (err) {
}
|