Commit 301d6345 301d63454d0f1bd9a337ce02f585d2a7d23828dd by Nicolas Perriault

continued migration to a full require() based layout

1 parent d5ff7c42
......@@ -106,6 +106,28 @@ phantom.casperArgs = (function(cliArgs) {
return extract;
})(phantom.args);
var sourceIds = {};
// Inspired by phantomjs-nodify: https://github.com/jgonera/phantomjs-nodify/
// TODO: remoive when phantomjs has js engine upgrade
if (!new Error().hasOwnProperty('stack')) {
Object.defineProperty(Error.prototype, 'stack', {
set: function(string) {
this._stack = string;
},
get: function() {
if (this._stack) {
return this._stack;
} else if (this.fileName || this.sourceId) {
return this.toString() + '\nat ' + getErrorMessage(this);
}
return this.toString() + '\nat unknown';
},
configurable: true,
enumerable: true
});
}
// Inspired by phantomjs-nodify: https://github.com/jgonera/phantomjs-nodify/
// TODO: remove when PhantomJS has full module support
require = (function(require, requireDir) {
......@@ -159,7 +181,14 @@ require = (function(require, requireDir) {
return requireCache[file].exports;
}
code = fs.read(file);
if (file.match(/\.coffee$/)) {
if (file.match(/\.js$/i)) {
try {
// TODO: esprima syntax check
} catch (e) {
e.fileName = file;
throw e;
}
} else if (file.match(/\.coffee$/i)) {
try {
code = CoffeeScript.compile(code);
} catch (e) {
......@@ -168,12 +197,12 @@ require = (function(require, requireDir) {
}
}
// a trick to associate Error's sourceId with file
//code += ";throw new Error('__sourceId__');";
code += ";throw new Error('__sourceId__');";
try {
fn = new Function('module', 'exports', code);
fn(module, module.exports);
} catch (e) {
if (typeof sourceIds === "object" && !sourceIds.hasOwnProperty(e.sourceId)) {
if (!sourceIds.hasOwnProperty(e.sourceId)) {
sourceIds[e.sourceId] = file;
}
if (e.message !== '__sourceId__') {
......
......@@ -28,12 +28,9 @@
var utils = require('./lib/utils');
exports.create = create;
exports.Casper = Casper;
function create(options) {
exports.create = function(options) {
return new Casper(options);
}
};
/**
* Main Casper object.
......@@ -89,7 +86,7 @@ var Casper = function(options) {
warning: 'COMMENT',
error: 'ERROR'
};
this.options = mergeObjects(this.defaults, options);
this.options = utils.mergeObjects(this.defaults, options);
this.page = null;
this.pendingWait = false;
this.requestUrl = 'about:blank';
......@@ -152,7 +149,7 @@ Casper.prototype = {
capture: function(targetFile, clipRect) {
var previousClipRect;
if (clipRect) {
if (!isType(clipRect, "object")) {
if (!utils.isType(clipRect, "object")) {
throw new Error("clipRect must be an Object instance.");
}
previousClipRect = this.page.clipRect;
......@@ -206,13 +203,13 @@ Casper.prototype = {
return;
}
var step = self.steps[self.step++];
if (isType(step, "function")) {
if (utils.isType(step, "function")) {
self.runStep(step);
} else {
self.result.time = new Date().getTime() - self.startTime;
self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info");
clearInterval(self.checker);
if (isType(onComplete, "function")) {
if (utils.isType(onComplete, "function")) {
try {
onComplete.call(self, self);
} catch (err) {
......@@ -234,7 +231,7 @@ Casper.prototype = {
* @return Boolean
*/
click: function(selector, fallbackToHref) {
fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
fallbackToHref = utils.isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
this.log("click on selector: " + selector, "debug");
return this.evaluate(function(selector, fallbackToHref) {
return __utils__.click(selector, fallbackToHref);
......@@ -252,10 +249,10 @@ Casper.prototype = {
* @return Function The final step function
*/
createStep: function(fn, options) {
if (!isType(fn, "function")) {
if (!utils.isType(fn, "function")) {
throw new Error("createStep(): a step definition must be a function");
}
fn.options = isType(options, "object") ? options : {};
fn.options = utils.isType(options, "object") ? options : {};
return fn;
},
......@@ -293,9 +290,9 @@ Casper.prototype = {
die: function(message, status) {
this.result.status = 'error';
this.result.time = new Date().getTime() - this.startTime;
message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
message = utils.isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
this.log(message, "error");
if (isType(this.options.onDie, "function")) {
if (utils.isType(this.options.onDie, "function")) {
this.options.onDie.call(this, this, message, status);
}
return this.exit(Number(status) > 0 ? Number(status) : 1);
......@@ -327,7 +324,7 @@ Casper.prototype = {
* @return Casper
*/
each: function(array, fn) {
if (!isType(array, "array")) {
if (!utils.isType(array, "array")) {
this.log("each() only works with arrays", "error");
return this;
}
......@@ -374,7 +371,7 @@ Casper.prototype = {
* @see WebPage#evaluate
*/
evaluate: function(fn, context) {
context = isType(context, "object") ? context : {};
context = utils.isType(context, "object") ? context : {};
var newFn = require('./lib/injector').create(fn).process(context);
return this.page.evaluate(newFn);
},
......@@ -454,10 +451,10 @@ Casper.prototype = {
*/
fill: function(selector, vals, submit) {
submit = submit === true ? submit : false;
if (!isType(selector, "string") || !selector.length) {
if (!utils.isType(selector, "string") || !selector.length) {
throw new Error("Form selector must be a non-empty string");
}
if (!isType(vals, "object")) {
if (!utils.isType(vals, "object")) {
throw new Error("Form values must be provided as an object");
}
var fillResults = this.evaluate(function(selector, values) {
......@@ -569,7 +566,7 @@ Casper.prototype = {
log: function(message, level, space) {
level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
space = space ? space : "phantom";
if (level === "error" && isType(this.options.onError, "function")) {
if (level === "error" && utils.isType(this.options.onError, "function")) {
this.options.onError.call(this, this, message, space);
}
if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
......@@ -581,7 +578,7 @@ Casper.prototype = {
message: message,
date: new Date().toString()
};
if (level in this.logFormats && isType(this.logFormats[level], "function")) {
if (level in this.logFormats && utils.isType(this.logFormats[level], "function")) {
message = this.logFormats[level](message, level, space);
} else {
var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]);
......@@ -603,7 +600,7 @@ Casper.prototype = {
* @return Casper
*/
open: function(location, options) {
options = isType(options, "object") ? options : {};
options = utils.isType(options, "object") ? options : {};
this.requestUrl = location;
// http auth
var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
......@@ -637,7 +634,7 @@ Casper.prototype = {
*/
resourceExists: function(test) {
var testFn;
if (isType(test, "string")) {
if (utils.isType(test, "string")) {
testFn = function (res) {
return res.url.search(test) !== -1;
};
......@@ -670,17 +667,17 @@ Casper.prototype = {
* @param Function step
*/
runStep: function(step) {
var skipLog = isType(step.options, "object") && step.options.skipLog === true;
var skipLog = utils.isType(step.options, "object") && step.options.skipLog === true;
var stepInfo = "Step " + (this.step) + "/" + this.steps.length;
var stepResult;
if (!skipLog) {
this.log(stepInfo + ' ' + this.getCurrentUrl() + ' (HTTP ' + this.currentHTTPStatus + ')', "info");
}
if (isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) {
if (utils.isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) {
var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) {
if (new Date().getTime() - start > self.options.stepTimeout) {
if (self.step == stepNum) {
if (isType(self.options.onStepTimeout, "function")) {
if (utils.isType(self.options.onStepTimeout, "function")) {
self.options.onStepTimeout.call(self, self);
} else {
self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
......@@ -699,7 +696,7 @@ Casper.prototype = {
throw e;
}
}
if (isType(this.options.onStepComplete, "function")) {
if (utils.isType(this.options.onStepComplete, "function")) {
this.options.onStepComplete.call(this, this, stepResult);
}
if (!skipLog) {
......@@ -718,7 +715,7 @@ Casper.prototype = {
if (!this.started) {
throw new Error("Casper must be started in order to use the setHttpAuth() method");
}
if (!isType(username, "string") || !isType(password, "string")) {
if (!utils.isType(username, "string") || !utils.isType(password, "string")) {
throw new Error("Both username and password must be strings");
}
this.page.settings.userName = username;
......@@ -746,37 +743,37 @@ Casper.prototype = {
this.options.logLevel = "warning";
}
// WebPage
if (!isWebPage(this.page)) {
if (isWebPage(this.options.page)) {
if (!utils.isWebPage(this.page)) {
if (utils.isWebPage(this.options.page)) {
this.page = this.options.page;
} else {
this.page = createPage(this);
}
}
this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings);
if (isType(this.options.clipRect, "object")) {
this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings);
if (utils.isType(this.options.clipRect, "object")) {
this.page.clipRect = this.options.clipRect;
}
if (isType(this.options.viewportSize, "object")) {
if (utils.isType(this.options.viewportSize, "object")) {
this.page.viewportSize = this.options.viewportSize;
}
this.started = true;
if (isType(this.options.timeout, "number") && this.options.timeout > 0) {
if (utils.isType(this.options.timeout, "number") && this.options.timeout > 0) {
this.log("Execution timeout set to " + this.options.timeout + 'ms', "info");
setTimeout(function(self) {
if (isType(self.options.onTimeout, "function")) {
if (utils.isType(self.options.onTimeout, "function")) {
self.options.onTimeout.call(self, self);
} else {
self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting.");
}
}, this.options.timeout, this);
}
if (isType(this.options.onPageInitialized, "function")) {
if (utils.isType(this.options.onPageInitialized, "function")) {
this.log("Post-configuring WebPage instance", "debug");
this.options.onPageInitialized.call(this, this.page);
}
if (isType(location, "string") && location.length > 0) {
return this.thenOpen(location, isType(then, "function") ? then : this.createStep(function(self) {
if (utils.isType(location, "string") && location.length > 0) {
return this.thenOpen(location, utils.isType(then, "function") ? then : this.createStep(function(self) {
self.log("start page is loaded", "debug");
}));
}
......@@ -793,7 +790,7 @@ Casper.prototype = {
if (!this.started) {
throw new Error("Casper not started; please use Casper#start");
}
if (!isType(step, "function")) {
if (!utils.isType(step, "function")) {
throw new Error("You can only define a step as a function");
}
// check if casper is running
......@@ -832,7 +829,7 @@ Casper.prototype = {
this.then(function(self) {
self.click(selector, fallbackToHref);
});
return isType(then, "function") ? this.then(then) : this;
return utils.isType(then, "function") ? this.then(then) : this;
},
/**
......@@ -864,7 +861,7 @@ Casper.prototype = {
}, {
skipLog: true
}));
return isType(then, "function") ? this.then(then) : this;
return utils.isType(then, "function") ? this.then(then) : this;
},
/**
......@@ -890,7 +887,7 @@ Casper.prototype = {
* @return Casper
*/
viewport: function(width, height) {
if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) {
if (!utils.isType(width, "number") || !utils.isType(height, "number") || width <= 0 || height <= 0) {
throw new Error("Invalid viewport width/height set: " + width + 'x' + height);
}
this.page.viewportSize = {
......@@ -910,10 +907,10 @@ Casper.prototype = {
*/
wait: function(timeout, then) {
timeout = Number(timeout, 10);
if (!isType(timeout, "number") || timeout < 1) {
if (!utils.isType(timeout, "number") || timeout < 1) {
this.die("wait() only accepts a positive integer > 0 as a timeout value");
}
if (then && !isType(then, "function")) {
if (then && !utils.isType(then, "function")) {
this.die("wait() a step definition must be a function");
}
return this.then(function(self) {
......@@ -947,10 +944,10 @@ Casper.prototype = {
*/
waitFor: function(testFx, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
if (!isType(testFx, "function")) {
if (!utils.isType(testFx, "function")) {
this.die("waitFor() needs a test function");
}
if (then && !isType(then, "function")) {
if (then && !utils.isType(then, "function")) {
this.die("waitFor() next step definition must be a function");
}
return this.then(function(self) {
......@@ -964,7 +961,7 @@ Casper.prototype = {
self.waitDone();
if (!condition) {
self.log("Casper.waitFor() timeout", "warning");
if (isType(onTimeout, "function")) {
if (utils.isType(onTimeout, "function")) {
onTimeout.call(self, self);
} else {
self.die("Timeout of " + timeout + "ms expired, exiting.", "error");
......@@ -1073,8 +1070,109 @@ Casper.prototype = {
* @param Object proto Prototype methods to add to Casper
*/
Casper.extend = function(proto) {
if (!isType(proto, "object")) {
if (!utils.isType(proto, "object")) {
throw new Error("extends() only accept objects as prototypes");
}
mergeObjects(Casper.prototype, proto);
};
exports.Casper = Casper;
/**
* Creates a new WebPage instance for Casper use.
*
* @param Casper casper A Casper instance
* @return WebPage
*/
function createPage(casper) {
var page;
if (phantom.version.major <= 1 && phantom.version.minor < 3 && utils.isType(require, "function")) {
page = new WebPage();
} else {
page = require('webpage').create();
}
page.onAlert = function(message) {
casper.log('[alert] ' + message, "info", "remote");
if (utils.isType(casper.options.onAlert, "function")) {
casper.options.onAlert.call(casper, casper, message);
}
};
page.onConsoleMessage = function(msg) {
var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg);
if (test && test.length === 3) {
level = test[1];
msg = test[2];
}
casper.log(msg, level, "remote");
};
page.onLoadStarted = function() {
casper.resources = [];
casper.loadInProgress = true;
};
page.onLoadFinished = function(status) {
if (status !== "success") {
var message = 'Loading resource failed with status=' + status;
if (casper.currentHTTPStatus) {
message += ' (HTTP ' + casper.currentHTTPStatus + ')';
}
message += ': ' + casper.requestUrl;
casper.log(message, "warning");
if (utils.isType(casper.options.onLoadError, "function")) {
casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
}
}
if (casper.options.clientScripts) {
if (betterTypeOf(casper.options.clientScripts) !== "array") {
casper.log("The clientScripts option must be an array", "error");
} else {
for (var i = 0; i < casper.options.clientScripts.length; i++) {
var script = casper.options.clientScripts[i];
if (casper.page.injectJs(script)) {
casper.log('Automatically injected ' + script + ' client side', "debug");
} else {
casper.log('Failed injecting ' + script + ' client side', "warning");
}
}
}
}
// Client-side utils injection
var injected = page.evaluate(replaceFunctionPlaceholders(function() {
eval("var ClientUtils = " + decodeURIComponent("%utils%"));
__utils__ = new ClientUtils();
return __utils__ instanceof ClientUtils;
}, {
utils: encodeURIComponent(require('./lib/clientutils').ClientUtils.toString())
}));
if (!injected) {
casper.log("Failed to inject Casper client-side utilities!", "warning");
} else {
casper.log("Successfully injected Casper client-side utilities", "debug");
}
// history
casper.history.push(casper.getCurrentUrl());
casper.loadInProgress = false;
};
page.onResourceReceived = function(resource) {
if (utils.isType(casper.options.onResourceReceived, "function")) {
casper.options.onResourceReceived.call(casper, casper, resource);
}
if (resource.stage === "end") {
casper.resources.push(resource);
}
if (resource.url === casper.requestUrl && resource.stage === "start") {
casper.currentHTTPStatus = resource.status;
if (utils.isType(casper.options.httpStatusHandlers, "object") &&
resource.status in casper.options.httpStatusHandlers &&
utils.isType(casper.options.httpStatusHandlers[resource.status], "function")) {
casper.options.httpStatusHandlers[resource.status].call(casper, casper, resource);
}
casper.currentUrl = resource.url;
}
};
page.onResourceRequested = function(request) {
if (utils.isType(casper.options.onResourceRequested, "function")) {
casper.options.onResourceRequested.call(casper, casper, request);
}
};
return page;
}
......
......@@ -26,418 +26,420 @@
*
*/
function create() {
return new ClientUtils();
}
/**
* Casper client-side helpers.
*/
var ClientUtils = function() {
var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var BASE64_DECODE_CHARS = new Array(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
);
/**
* Clicks on the DOM element behind the provided selector.
*
* @param String selector A CSS3 selector to the element to click
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Boolean
*/
this.click = function(selector, fallbackToHref) {
fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref;
var elem = this.findOne(selector);
if (!elem) {
return false;
}
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem);
if (elem.dispatchEvent(evt)) {
return true;
}
if (fallbackToHref && elem.hasAttribute('href')) {
document.location = elem.getAttribute('href');
return true;
}
return false;
(function(exports) {
exports.create = function() {
return new ClientUtils();
};
/**
* Decodes a base64 encoded string. Succeeds where window.atob() fails.
*
* @param String str The base64 encoded contents
* @return string
* Casper client-side helpers.
*/
this.decode = function(str) {
var c1, c2, c3, c4, i = 0, len = str.length, out = "";
while (i < len) {
do {
c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
} while (i < len && c1 == -1);
if (c1 == -1) {
break;
var ClientUtils = function() {
var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var BASE64_DECODE_CHARS = new Array(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
);
/**
* Clicks on the DOM element behind the provided selector.
*
* @param String selector A CSS3 selector to the element to click
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Boolean
*/
this.click = function(selector, fallbackToHref) {
fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref;
var elem = this.findOne(selector);
if (!elem) {
return false;
}
do {
c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
} while (i < len && c2 == -1);
if (c2 == -1) {
break;
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem);
if (elem.dispatchEvent(evt)) {
return true;
}
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
do {
c3 = str.charCodeAt(i++) & 0xff;
if (c3 == 61)
return out;
c3 = BASE64_DECODE_CHARS[c3];
} while (i < len && c3 == -1);
if (c3 == -1) {
break;
if (fallbackToHref && elem.hasAttribute('href')) {
document.location = elem.getAttribute('href');
return true;
}
out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
do {
c4 = str.charCodeAt(i++) & 0xff;
if (c4 == 61) {
return false;
};
/**
* Decodes a base64 encoded string. Succeeds where window.atob() fails.
*
* @param String str The base64 encoded contents
* @return string
*/
this.decode = function(str) {
var c1, c2, c3, c4, i = 0, len = str.length, out = "";
while (i < len) {
do {
c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
} while (i < len && c1 == -1);
if (c1 == -1) {
break;
}
do {
c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
} while (i < len && c2 == -1);
if (c2 == -1) {
break;
}
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
do {
c3 = str.charCodeAt(i++) & 0xff;
if (c3 == 61)
return out;
c3 = BASE64_DECODE_CHARS[c3];
} while (i < len && c3 == -1);
if (c3 == -1) {
break;
}
out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
do {
c4 = str.charCodeAt(i++) & 0xff;
if (c4 == 61) {
return out;
}
c4 = BASE64_DECODE_CHARS[c4];
} while (i < len && c4 == -1);
if (c4 == -1) {
break;
}
c4 = BASE64_DECODE_CHARS[c4];
} while (i < len && c4 == -1);
if (c4 == -1) {
break;
out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
}
out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
}
return out;
};
return out;
};
/**
* Base64 encodes a string, even binary ones. Succeeds where
* window.btoa() fails.
*
* @param String str The string content to encode
* @return string
*/
this.encode = function(str) {
var out = "", i = 0, len = str.length, c1, c2, c3;
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i == len) {
out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4);
out += "==";
break;
}
c2 = str.charCodeAt(i++);
if (i == len) {
/**
* Base64 encodes a string, even binary ones. Succeeds where
* window.btoa() fails.
*
* @param String str The string content to encode
* @return string
*/
this.encode = function(str) {
var out = "", i = 0, len = str.length, c1, c2, c3;
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i == len) {
out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4);
out += "==";
break;
}
c2 = str.charCodeAt(i++);
if (i == len) {
out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2);
out += "=";
break;
out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
out += BASE64_ENCODE_CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F);
}
c3 = str.charCodeAt(i++);
out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
out += BASE64_ENCODE_CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F);
}
return out;
};
/**
* Checks if a given DOM element exists in remote page.
*
* @param String selector CSS3 selector
* @return Boolean
*/
this.exists = function(selector) {
try {
return document.querySelectorAll(selector).length > 0;
} catch (e) {
return false;
}
};
/**
* Checks if a given DOM element is visible in remote page.
*
* @param String selector CSS3 selector
* @return Boolean
*/
this.visible = function(selector) {
try {
var el = document.querySelector(selector);
return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0;
} catch (e) {
return false;
}
};
/**
* Fetches innerText within the element(s) matching a given CSS3
* selector.
*
* @param String selector A CSS3 selector
* @return String
*/
this.fetchText = function(selector) {
var text = '', elements = this.findAll(selector);
if (elements && elements.length) {
Array.prototype.forEach.call(elements, function(element) {
text += element.innerText;
});
}
return text;
};
return out;
};
/**
* Fills a form with provided field values, and optionnaly submits it.
*
* @param HTMLElement|String form A form element, or a CSS3 selector to a form element
* @param Object vals Field values
* @return Object An object containing setting result for each field, including file uploads
*/
this.fill = function(form, vals) {
var out = {
errors: [],
fields: [],
files: []
/**
* Checks if a given DOM element exists in remote page.
*
* @param String selector CSS3 selector
* @return Boolean
*/
this.exists = function(selector) {
try {
return document.querySelectorAll(selector).length > 0;
} catch (e) {
return false;
}
};
if (!(form instanceof HTMLElement) || typeof form === "string") {
__utils__.log("attempting to fetch form element from selector: '" + form + "'", "info");
/**
* Checks if a given DOM element is visible in remote page.
*
* @param String selector CSS3 selector
* @return Boolean
*/
this.visible = function(selector) {
try {
form = document.querySelector(form);
var el = document.querySelector(selector);
return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0;
} catch (e) {
if (e.name === "SYNTAX_ERR") {
out.errors.push("invalid form selector provided: '" + form + "'");
return out;
}
return false;
}
}
if (!form) {
out.errors.push("form not found");
return out;
}
for (var name in vals) {
if (!vals.hasOwnProperty(name)) {
continue;
};
/**
* Fetches innerText within the element(s) matching a given CSS3
* selector.
*
* @param String selector A CSS3 selector
* @return String
*/
this.fetchText = function(selector) {
var text = '', elements = this.findAll(selector);
if (elements && elements.length) {
Array.prototype.forEach.call(elements, function(element) {
text += element.innerText;
});
}
var field = form.querySelectorAll('[name="' + name + '"]');
var value = vals[name];
if (!field) {
out.errors.push('no field named "' + name + '" in form');
continue;
return text;
};
/**
* Fills a form with provided field values, and optionnaly submits it.
*
* @param HTMLElement|String form A form element, or a CSS3 selector to a form element
* @param Object vals Field values
* @return Object An object containing setting result for each field, including file uploads
*/
this.fill = function(form, vals) {
var out = {
errors: [],
fields: [],
files: []
};
if (!(form instanceof HTMLElement) || typeof form === "string") {
__utils__.log("attempting to fetch form element from selector: '" + form + "'", "info");
try {
form = document.querySelector(form);
} catch (e) {
if (e.name === "SYNTAX_ERR") {
out.errors.push("invalid form selector provided: '" + form + "'");
return out;
}
}
}
try {
out.fields[name] = this.setField(field, value);
} catch (err) {
if (err.name === "FileUploadError") {
out.files.push({
name: name,
path: err.path
});
} else {
this.log(err, "error");
throw err;
if (!form) {
out.errors.push("form not found");
return out;
}
for (var name in vals) {
if (!vals.hasOwnProperty(name)) {
continue;
}
var field = form.querySelectorAll('[name="' + name + '"]');
var value = vals[name];
if (!field) {
out.errors.push('no field named "' + name + '" in form');
continue;
}
try {
out.fields[name] = this.setField(field, value);
} catch (err) {
if (err.name === "FileUploadError") {
out.files.push({
name: name,
path: err.path
});
} else {
this.log(err, "error");
throw err;
}
}
}
}
return out;
};
return out;
};
/**
* Finds all DOM elements matching by the provided selector.
*
* @param String selector CSS3 selector
* @return NodeList|undefined
*/
this.findAll = function(selector) {
try {
return document.querySelectorAll(selector);
} catch (e) {
this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
}
};
/**
* Finds all DOM elements matching by the provided selector.
*
* @param String selector CSS3 selector
* @return NodeList|undefined
*/
this.findAll = function(selector) {
try {
return document.querySelectorAll(selector);
} catch (e) {
this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
}
};
/**
* Finds a DOM element by the provided selector.
*
* @param String selector CSS3 selector
* @return HTMLElement|undefined
*/
this.findOne = function(selector) {
try {
return document.querySelector(selector);
} catch (e) {
this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors");
}
};
/**
* Finds a DOM element by the provided selector.
*
* @param String selector CSS3 selector
* @return HTMLElement|undefined
*/
this.findOne = function(selector) {
try {
return document.querySelector(selector);
} catch (e) {
this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors");
}
};
/**
* Downloads a resource behind an url and returns its base64-encoded
* contents.
*
* @param String url The resource url
* @param String method The request method, optional (default: GET)
* @param Object data The request data, optional
* @return String Base64 contents string
*/
this.getBase64 = function(url, method, data) {
return this.encode(this.getBinary(url, method, data));
};
/**
* Downloads a resource behind an url and returns its base64-encoded
* contents.
*
* @param String url The resource url
* @param String method The request method, optional (default: GET)
* @param Object data The request data, optional
* @return String Base64 contents string
*/
this.getBase64 = function(url, method, data) {
return this.encode(this.getBinary(url, method, data));
};
/**
* Retrieves string contents from a binary file behind an url. Silently
* fails but log errors.
*
* @param String url
* @param String method
* @param Object data
* @return string
*/
this.getBinary = function(url, method, data) {
try {
var xhr = new XMLHttpRequest(), dataString = "";
if (typeof method !== "string" || ["GET", "POST"].indexOf(method.toUpperCase()) === -1) {
method = "GET";
} else {
method = method.toUpperCase();
}
xhr.open(method, url, false);
this.log("getBinary(): Using HTTP method: '" + method + "'", "debug");
xhr.overrideMimeType("text/plain; charset=x-user-defined");
if (method === "POST") {
if (typeof data === "object") {
var dataList = [];
for (var k in data) {
dataList.push(escape(k) + "=" + escape(data[k].toString()));
/**
* Retrieves string contents from a binary file behind an url. Silently
* fails but log errors.
*
* @param String url
* @param String method
* @param Object data
* @return string
*/
this.getBinary = function(url, method, data) {
try {
var xhr = new XMLHttpRequest(), dataString = "";
if (typeof method !== "string" || ["GET", "POST"].indexOf(method.toUpperCase()) === -1) {
method = "GET";
} else {
method = method.toUpperCase();
}
xhr.open(method, url, false);
this.log("getBinary(): Using HTTP method: '" + method + "'", "debug");
xhr.overrideMimeType("text/plain; charset=x-user-defined");
if (method === "POST") {
if (typeof data === "object") {
var dataList = [];
for (var k in data) {
dataList.push(escape(k) + "=" + escape(data[k].toString()));
}
dataString = dataList.join('&');
this.log("getBinary(): Using request data: '" + dataString + "'", "debug");
}
dataString = dataList.join('&');
this.log("getBinary(): Using request data: '" + dataString + "'", "debug");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
xhr.send(method === "POST" ? dataString : null);
return xhr.responseText;
} catch (e) {
if (e.name === "NETWORK_ERR" && e.code === 101) {
this.log("getBinary(): Unfortunately, casperjs cannot make cross domain ajax requests", "warning");
xhr.send(method === "POST" ? dataString : null);
return xhr.responseText;
} catch (e) {
if (e.name === "NETWORK_ERR" && e.code === 101) {
this.log("getBinary(): Unfortunately, casperjs cannot make cross domain ajax requests", "warning");
}
this.log("getBinary(): Error while fetching " + url + ": " + e, "error");
return "";
}
this.log("getBinary(): Error while fetching " + url + ": " + e, "error");
return "";
}
};
};
/**
* Logs a message.
*
* @param String message
* @param String level
*/
this.log = function(message, level) {
console.log("[casper:" + (level || "debug") + "] " + message);
};
/**
* Logs a message.
*
* @param String message
* @param String level
*/
this.log = function(message, level) {
console.log("[casper:" + (level || "debug") + "] " + message);
};
/**
* Sets a field (or a set of fields) value. Fails silently, but log
* error messages.
*
* @param HTMLElement|NodeList field One or more element defining a field
* @param mixed value The field value to set
*/
this.setField = function(field, value) {
var fields, out;
value = value || "";
if (field instanceof NodeList) {
fields = field;
field = fields[0];
}
if (!field instanceof HTMLElement) {
this.log("invalid field type; only HTMLElement and NodeList are supported", "error");
}
this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug");
try {
field.focus();
} catch (e) {
__utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
}
var nodeName = field.nodeName.toLowerCase();
switch (nodeName) {
case "input":
var type = field.getAttribute('type') || "text";
switch (type.toLowerCase()) {
case "color":
case "date":
case "datetime":
case "datetime-local":
case "email":
case "hidden":
case "month":
case "number":
case "password":
case "range":
case "search":
case "tel":
case "text":
case "time":
case "url":
case "week":
field.value = value;
break;
case "checkbox":
if (fields.length > 1) {
var values = value;
if (!Array.isArray(values)) {
values = [values];
/**
* Sets a field (or a set of fields) value. Fails silently, but log
* error messages.
*
* @param HTMLElement|NodeList field One or more element defining a field
* @param mixed value The field value to set
*/
this.setField = function(field, value) {
var fields, out;
value = value || "";
if (field instanceof NodeList) {
fields = field;
field = fields[0];
}
if (!field instanceof HTMLElement) {
this.log("invalid field type; only HTMLElement and NodeList are supported", "error");
}
this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug");
try {
field.focus();
} catch (e) {
__utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
}
var nodeName = field.nodeName.toLowerCase();
switch (nodeName) {
case "input":
var type = field.getAttribute('type') || "text";
switch (type.toLowerCase()) {
case "color":
case "date":
case "datetime":
case "datetime-local":
case "email":
case "hidden":
case "month":
case "number":
case "password":
case "range":
case "search":
case "tel":
case "text":
case "time":
case "url":
case "week":
field.value = value;
break;
case "checkbox":
if (fields.length > 1) {
var values = value;
if (!Array.isArray(values)) {
values = [values];
}
Array.prototype.forEach.call(fields, function(f) {
f.checked = values.indexOf(f.value) !== -1 ? true : false;
});
} else {
field.checked = value ? true : false;
}
Array.prototype.forEach.call(fields, function(f) {
f.checked = values.indexOf(f.value) !== -1 ? true : false;
});
} else {
field.checked = value ? true : false;
}
break;
case "file":
throw {
name: "FileUploadError",
message: "file field must be filled using page.uploadFile",
path: value
};
case "radio":
if (fields) {
Array.prototype.forEach.call(fields, function(e) {
e.checked = (e.value === value);
});
} else {
out = 'provided radio elements are empty';
}
break;
default:
out = "unsupported input field type: " + type;
break;
}
break;
case "select":
case "textarea":
field.value = value;
break;
default:
out = 'unsupported field type: ' + nodeName;
break;
}
try {
field.blur();
} catch (err) {
__utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
}
return out;
break;
case "file":
throw {
name: "FileUploadError",
message: "file field must be filled using page.uploadFile",
path: value
};
case "radio":
if (fields) {
Array.prototype.forEach.call(fields, function(e) {
e.checked = (e.value === value);
});
} else {
out = 'provided radio elements are empty';
}
break;
default:
out = "unsupported input field type: " + type;
break;
}
break;
case "select":
case "textarea":
field.value = value;
break;
default:
out = 'unsupported field type: ' + nodeName;
break;
}
try {
field.blur();
} catch (err) {
__utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
}
return out;
};
};
};
})(exports || {});
......
......@@ -26,12 +26,9 @@
*
*/
exports.create = create;
exports.Colorizer = Colorizer;
function create() {
exports.create = function() {
return new Colorizer();
}
};
/**
* This is a port of lime colorizer.
......@@ -95,3 +92,4 @@ var Colorizer = function() {
return "\033[" + codes.join(';') + 'm' + text + "\033[0m";
};
};
exports.Colorizer = Colorizer;
......
......@@ -27,15 +27,15 @@
*/
exports.create = create;
exports.FunctionArgsInjector = FunctionArgsInjector;
function create(fn) {
exports.create = function(fn) {
return new FunctionArgsInjector(fn);
}
};
/**
* Function argument injector.
*
* FIXME: use new Function() instead of eval()
*/
var FunctionArgsInjector = function(fn) {
if (!isType(fn, "function")) {
......@@ -82,3 +82,4 @@ var FunctionArgsInjector = function(fn) {
return inject.join('\n') + '\n';
};
};
exports.FunctionArgsInjector = FunctionArgsInjector;
......
......@@ -27,10 +27,11 @@
*/
var fs = require('fs');
var utils = require('./lib/utils');
function create(casper, options) {
exports.create = function(casper, options) {
return new Tester(casper, options);
}
};
/**
* Casper tester: makes assertions, stores test results and display then.
......@@ -39,9 +40,9 @@ function create(casper, options) {
var Tester = function(casper, options) {
this.running = false;
this.suites = [];
this.options = isType(options, "object") ? options : {};
this.options = utils.isType(options, "object") ? options : {};
if (!casper instanceof require('./lib/casper').Casper) {
if (!utils.isCasperObject(casper)) {
throw new Error("Tester needs a Casper instance");
}
......@@ -219,7 +220,7 @@ var Tester = function(casper, options) {
* @param String message Test description
*/
this.assertType = function(input, type, message) {
return this.assertEquals(betterTypeOf(input), type, message);
return this.assertEquals(utils.betterTypeOf(input), type, message);
};
/**
......@@ -368,7 +369,7 @@ var Tester = function(casper, options) {
* @param Boolean exit
*/
this.renderResults = function(exit, status, save) {
save = isType(save, "string") ? save : this.options.save;
save = utils.isType(save, "string") ? save : this.options.save;
var total = this.testResults.passed + this.testResults.failed, statusText, style, result;
if (this.testResults.failed > 0) {
statusText = FAIL;
......@@ -379,7 +380,7 @@ var Tester = function(casper, options) {
}
result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.';
casper.echo(this.colorize(fillBlanks(result), style));
if (save && isType(require, "function")) {
if (save && utils.isType(require, "function")) {
try {
fs.write(save, exporter.getXML(), 'w');
casper.echo('result log stored in ' + save, 'INFO');
......@@ -455,10 +456,10 @@ var Tester = function(casper, options) {
* @param Boolean
*/
this.testEquals = function(v1, v2) {
if (betterTypeOf(v1) !== betterTypeOf(v2)) {
if (utils.betterTypeOf(v1) !== utils.betterTypeOf(v2)) {
return false;
}
if (isType(v1, "function")) {
if (utils.isType(v1, "function")) {
return v1.toString() === v2.toString();
}
if (v1 instanceof Object && v2 instanceof Object) {
......@@ -475,3 +476,4 @@ var Tester = function(casper, options) {
return v1 === v2;
};
};
exports.Tester = Tester;
......
......@@ -41,105 +41,7 @@ function betterTypeOf(input) {
return typeof input;
}
}
/**
* Creates a new WebPage instance for Casper use.
*
* @param Casper casper A Casper instance
* @return WebPage
*/
function createPage(casper) {
var page;
if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
page = new WebPage();
} else {
page = require('webpage').create();
}
page.onAlert = function(message) {
casper.log('[alert] ' + message, "info", "remote");
if (isType(casper.options.onAlert, "function")) {
casper.options.onAlert.call(casper, casper, message);
}
};
page.onConsoleMessage = function(msg) {
var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg);
if (test && test.length === 3) {
level = test[1];
msg = test[2];
}
casper.log(msg, level, "remote");
};
page.onLoadStarted = function() {
casper.resources = [];
casper.loadInProgress = true;
};
page.onLoadFinished = function(status) {
if (status !== "success") {
var message = 'Loading resource failed with status=' + status;
if (casper.currentHTTPStatus) {
message += ' (HTTP ' + casper.currentHTTPStatus + ')';
}
message += ': ' + casper.requestUrl;
casper.log(message, "warning");
if (isType(casper.options.onLoadError, "function")) {
casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
}
}
if (casper.options.clientScripts) {
if (betterTypeOf(casper.options.clientScripts) !== "array") {
casper.log("The clientScripts option must be an array", "error");
} else {
for (var i = 0; i < casper.options.clientScripts.length; i++) {
var script = casper.options.clientScripts[i];
if (casper.page.injectJs(script)) {
casper.log('Automatically injected ' + script + ' client side', "debug");
} else {
casper.log('Failed injecting ' + script + ' client side', "warning");
}
}
}
}
// Client-side utils injection
var injected = page.evaluate(replaceFunctionPlaceholders(function() {
eval("var ClientUtils = " + decodeURIComponent("%utils%"));
__utils__ = new ClientUtils();
return __utils__ instanceof ClientUtils;
}, {
utils: encodeURIComponent(require('./lib/clientutils').ClientUtils.toString())
}));
if (!injected) {
casper.log("Failed to inject Casper client-side utilities!", "warning");
} else {
casper.log("Successfully injected Casper client-side utilities", "debug");
}
// history
casper.history.push(casper.getCurrentUrl());
casper.loadInProgress = false;
};
page.onResourceReceived = function(resource) {
if (isType(casper.options.onResourceReceived, "function")) {
casper.options.onResourceReceived.call(casper, casper, resource);
}
if (resource.stage === "end") {
casper.resources.push(resource);
}
if (resource.url === casper.requestUrl && resource.stage === "start") {
casper.currentHTTPStatus = resource.status;
if (isType(casper.options.httpStatusHandlers, "object") &&
resource.status in casper.options.httpStatusHandlers &&
isType(casper.options.httpStatusHandlers[resource.status], "function")) {
casper.options.httpStatusHandlers[resource.status].call(casper, casper, resource);
}
casper.currentUrl = resource.url;
}
};
page.onResourceRequested = function(request) {
if (isType(casper.options.onResourceRequested, "function")) {
casper.options.onResourceRequested.call(casper, casper, request);
}
};
return page;
}
exports.betterTypeOf = betterTypeOf;
/**
* Dumps a JSON representation of passed value to the console. Used for
......@@ -150,6 +52,7 @@ function createPage(casper) {
function dump(value) {
console.log(serialize(value));
}
exports.dump = dump;
/**
* Returns the file extension in lower case.
......@@ -164,6 +67,7 @@ function fileExt(file) {
return '';
}
}
exports.fileExt = fileExt;
/**
* Takes a string and append blank until the pad value is reached.
......@@ -179,6 +83,18 @@ function fillBlanks(text, pad) {
}
return text;
}
exports.fillBlanks = fillBlanks;
/**
* Checks if passed argument is an instance of Capser object.
*
* @param mixed value
* @return Boolean
*/
function isCasperObject(value) {
return value instanceof require('./lib/casper').Casper;
}
exports.isCasperObject = isCasperObject;
/**
* Checks if a file is apparently javascript compatible (.js or .coffee).
......@@ -190,6 +106,7 @@ function isJsFile(file) {
var ext = fileExt(file);
return isType(ext, "string") && ['js', 'coffee'].indexOf(ext) !== -1;
}
exports.isJsFile = isJsFile;
/**
* Shorthands for checking if a value is of the given type. Can check for
......@@ -202,6 +119,7 @@ function isJsFile(file) {
function isType(what, typeName) {
return betterTypeOf(what) === typeName;
}
exports.isType = isType;
/**
* Checks if the provided var is a WebPage instance
......@@ -219,6 +137,7 @@ function isWebPage(what) {
return what.toString().indexOf('WebPage(') === 0;
}
}
exports.isWebPage = isWebPage;
/**
* Object recursive merging utility.
......@@ -241,6 +160,7 @@ function mergeObjects(obj1, obj2) {
}
return obj1;
}
exports.mergeObjects = mergeObjects;
/**
* Replaces a function string contents with placeholders provided by an
......@@ -262,6 +182,7 @@ function replaceFunctionPlaceholders(fn, replacements) {
}
return fn;
}
exports.replaceFunctionPlaceholders = replaceFunctionPlaceholders;
/**
* Serializes a value using JSON.
......@@ -277,3 +198,4 @@ function serialize(value) {
}
return JSON.stringify(value, null, 4);
}
exports.serialize = serialize;
......
......@@ -26,12 +26,9 @@
*
*/
exports.create = create;
exports.XUnitExporter = XUnitExporter;
function create() {
exports.create = function() {
return new XUnitExporter();
}
};
/**
* JUnit XML (xUnit) exporter for test results.
......@@ -97,3 +94,5 @@ XUnitExporter = function() {
return xml;
};
};
exports.XUnitExporter = XUnitExporter;
......