Commit 0d0e9621 0d0e962148663dd34fc54a8b27d1b481bc3d4c62 by Nicolas Perriault

added events where applicable in Casper methods

1 parent 582f1f9a
......@@ -11,7 +11,7 @@ def resolve(path):
return path
CASPER_PATH = os.path.abspath(os.path.join(os.path.dirname(resolve(__file__)), '..'))
CASPER_ARGS = ['phantomjs', os.path.join(CASPER_PATH, 'bin', 'bootstrap.js'), '--casper-path=%s' % CASPER_PATH, '--cli']
CASPER_ARGS = ['phantomjs14', os.path.join(CASPER_PATH, 'bin', 'bootstrap.js'), '--casper-path=%s' % CASPER_PATH, '--cli']
CASPER_ARGS.extend(sys.argv[1:])
try:
......
......@@ -26,6 +26,7 @@
*
*/
var events = require('events');
var fs = require('fs');
var utils = require('utils');
......@@ -104,992 +105,1017 @@ var Casper = function(options) {
this.test = require('tester').create(this);
};
utils.inherits(Casper, events.EventEmitter);
/**
* Casper prototype
* Go a step back in browser's history
*
* @return Casper
*/
Casper.prototype = {
/**
* Go a step back in browser's history
*
* @return Casper
*/
back: function() {
return this.then(function(self) {
self.evaluate(function() {
history.back();
});
Casper.prototype.back = function() {
return this.then(function() {
this.emit('casper.back');
this.evaluate(function() {
history.back();
});
},
});
};
/**
* Encodes a resource using the base64 algorithm synchroneously using
* client-side XMLHttpRequest.
*
* NOTE: we cannot use window.btoa() for some strange reasons here.
*
* @param String url The url to download
* @param String method The method to use, optional: default GET
* @param String data The data to send, optional
* @return string Base64 encoded result
*/
base64encode: function(url, method, data) {
return this.evaluate(function(url, method, data) {
return __utils__.getBase64(url, method, data);
}, { url: url, method: method, data: data });
},
/**
* Encodes a resource using the base64 algorithm synchroneously using
* client-side XMLHttpRequest.
*
* NOTE: we cannot use window.btoa() for some strange reasons here.
*
* @param String url The url to download
* @param String method The method to use, optional: default GET
* @param String data The data to send, optional
* @return string Base64 encoded result
*/
Casper.prototype.base64encode = function(url, method, data) {
return this.evaluate(function(url, method, data) {
return __utils__.getBase64(url, method, data);
}, { url: url, method: method, data: data });
};
/**
* Proxy method for WebPage#render. Adds a clipRect parameter for
* automatically set page clipRect setting values and sets it back once
* done. If the cliprect parameter is omitted, the full page viewport
* area will be rendered.
*
* @param String targetFile A target filename
* @param mixed clipRect An optional clipRect object (optional)
* @return Casper
*/
capture: function(targetFile, clipRect) {
var previousClipRect;
targetFile = fs.absolute(targetFile);
if (clipRect) {
if (!utils.isClipRect(clipRect)) {
throw new Error("clipRect must be a valid ClipRect object.");
}
previousClipRect = this.page.clipRect;
this.page.clipRect = clipRect;
this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug");
} else {
this.log('Capturing page to ' + targetFile, "debug");
}
if (!this.page.render(targetFile)) {
this.log('Failed to save screenshot to ' + targetFile + '; please check permissions', "error");
}
if (previousClipRect) {
this.page.clipRect = previousClipRect;
/**
* Proxy method for WebPage#render. Adds a clipRect parameter for
* automatically set page clipRect setting values and sets it back once
* done. If the cliprect parameter is omitted, the full page viewport
* area will be rendered.
*
* @param String targetFile A target filename
* @param mixed clipRect An optional clipRect object (optional)
* @return Casper
*/
Casper.prototype.capture = function(targetFile, clipRect) {
var previousClipRect;
targetFile = fs.absolute(targetFile);
if (clipRect) {
if (!utils.isClipRect(clipRect)) {
throw new Error("clipRect must be a valid ClipRect object.");
}
return this;
},
previousClipRect = this.page.clipRect;
this.page.clipRect = clipRect;
this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug");
} else {
this.log('Capturing page to ' + targetFile, "debug");
}
if (!this.page.render(targetFile)) {
this.log('Failed to save screenshot to ' + targetFile + '; please check permissions', "error");
}
if (previousClipRect) {
this.page.clipRect = previousClipRect;
}
return this;
};
/**
* Captures the page area containing the provided selector.
*
* @param String targetFile Target destination file path.
* @param String selector CSS3 selector
* @return Casper
*/
captureSelector: function(targetFile, selector) {
return this.capture(targetFile, this.getElementBounds(selector));
},
/**
* Captures the page area containing the provided selector.
*
* @param String targetFile Target destination file path.
* @param String selector CSS3 selector
* @return Casper
*/
Casper.prototype.captureSelector = function(targetFile, selector) {
return this.capture(targetFile, this.getElementBounds(selector));
};
/**
* Checks for any further navigation step to process.
*
* @param Casper self A self reference
* @param function onComplete An options callback to apply on completion
*/
checkStep: function(self, onComplete) {
if (self.pendingWait || self.loadInProgress) {
return;
}
var step = self.steps[self.step++];
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");
self.page.content = ''; // avoid having previously loaded DOM contents being still active (refs #34)
clearInterval(self.checker);
if (utils.isType(onComplete, "function")) {
try {
onComplete.call(self, self);
} catch (err) {
self.log("Could not complete final step: " + err, "error");
}
} else {
// default behavior is to exit phantom
self.exit();
/**
* Checks for any further navigation step to process.
*
* @param Casper self A self reference
* @param function onComplete An options callback to apply on completion
*/
Casper.prototype.checkStep = function(self, onComplete) {
if (self.pendingWait || self.loadInProgress) {
return;
}
var step = self.steps[self.step++];
if (utils.isFunction(step)) {
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");
self.page.content = ''; // avoid having previously loaded DOM contents being still active (refs #34)
clearInterval(self.checker);
if (utils.isFunction(onComplete)) {
try {
onComplete.call(self, self);
} catch (err) {
self.log("Could not complete final step: " + err, "error");
}
} else {
// default behavior is to exit phantom
self.exit();
}
},
}
};
/**
* Emulates a click on the element from the provided selector, if
* possible. In case of success, `true` is returned.
*
* @param String selector A DOM CSS3 compatible selector
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Boolean
*/
click: function(selector, 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);
}, {
selector: selector,
fallbackToHref: fallbackToHref
});
},
/**
* Emulates a click on the element from the provided selector, if
* possible. In case of success, `true` is returned.
*
* @param String selector A DOM CSS3 compatible selector
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Boolean
*/
Casper.prototype.click = function(selector, fallbackToHref) {
fallbackToHref = utils.isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
this.log("Click on selector: " + selector, "debug");
this.emit('casper.click', selector, fallbackToHref);
return this.evaluate(function(selector, fallbackToHref) {
return __utils__.click(selector, fallbackToHref);
}, {
selector: selector,
fallbackToHref: fallbackToHref
});
};
/**
* Creates a step definition.
*
* @param Function fn The step function to call
* @param Object options Step options
* @return Function The final step function
*/
createStep: function(fn, options) {
if (!utils.isType(fn, "function")) {
throw new Error("createStep(): a step definition must be a function");
}
fn.options = utils.isType(options, "object") ? options : {};
return fn;
},
/**
* Creates a step definition.
*
* @param Function fn The step function to call
* @param Object options Step options
* @return Function The final step function
*/
Casper.prototype.createStep = function(fn, options) {
if (!utils.isFunction(fn)) {
throw new Error("createStep(): a step definition must be a function");
}
fn.options = utils.isObject(options) ? options : {};
this.emit('casper.step.created', fn);
return fn;
};
/**
* Logs the HTML code of the current page.
*
* @return Casper
*/
debugHTML: function() {
this.echo(this.evaluate(function() {
return document.body.innerHTML;
}));
return this;
},
/**
* Logs the HTML code of the current page.
*
* @return Casper
*/
Casper.prototype.debugHTML = function() {
this.echo(this.evaluate(function() {
return document.body.innerHTML;
}));
return this;
};
/**
* Logs the textual contents of the current page.
*
* @return Casper
*/
debugPage: function() {
this.echo(this.evaluate(function() {
return document.body.innerText;
}));
return this;
},
/**
* Logs the textual contents of the current page.
*
* @return Casper
*/
Casper.prototype.debugPage = function() {
this.echo(this.evaluate(function() {
return document.body.innerText;
}));
return this;
};
/**
* Exit phantom on failure, with a logged error message.
*
* @param String message An optional error message
* @param Number status An optional exit status code (must be > 0)
* @return Casper
*/
die: function(message, status) {
this.result.status = 'error';
this.result.time = new Date().getTime() - this.startTime;
message = utils.isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
this.log(message, "error");
if (utils.isType(this.options.onDie, "function")) {
this.options.onDie.call(this, this, message, status);
}
return this.exit(Number(status) > 0 ? Number(status) : 1);
},
/**
* Exit phantom on failure, with a logged error message.
*
* @param String message An optional error message
* @param Number status An optional exit status code (must be > 0)
* @return Casper
*/
Casper.prototype.die = function(message, status) {
this.result.status = "error";
this.result.time = new Date().getTime() - this.startTime;
message = utils.isString(message) && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
this.log(message, "error");
this.emit('casper.die', message, status);
if (utils.isFunction(this.options.onDie)) {
this.options.onDie.call(this, this, message, status);
}
return this.exit(~~status > 0 ? ~~status : 1);
};
/**
* Downloads a resource and saves it on the filesystem.
*
* @param String url The url of the resource to download
* @param String targetPath The destination file path
* @return Casper
*/
download: function(url, targetPath) {
var cu = require('clientutils').create();
try {
fs.write(targetPath, cu.decode(this.base64encode(url)), 'w');
} catch (e) {
this.log("Error while downloading " + url + " to " + targetPath + ": " + e, "error");
}
return this;
},
/**
* Downloads a resource and saves it on the filesystem.
*
* @param String url The url of the resource to download
* @param String targetPath The destination file path
* @return Casper
*/
Casper.prototype.download = function(url, targetPath) {
var cu = require('clientutils').create();
try {
fs.write(targetPath, cu.decode(this.base64encode(url)), 'w');
} catch (e) {
this.log("Error while downloading " + url + " to " + targetPath + ": " + e, "error");
}
return this;
};
/**
* Iterates over the values of a provided array and execute a callback
* for each item.
*
* @param Array array
* @param Function fn Callback: function(self, item, index)
* @return Casper
*/
each: function(array, fn) {
if (!utils.isType(array, "array")) {
this.log("each() only works with arrays", "error");
return this;
}
(function(self) {
array.forEach(function(item, i) {
fn.call(self, self, item, i);
});
})(this);
/**
* Iterates over the values of a provided array and execute a callback
* for @ item.
*
* @param Array array
* @param Function fn Callback: function(self, item, index)
* @return Casper
*/
Casper.prototype.each = function(array, fn) {
if (!utils.isArray(array)) {
this.log("each() only works with arrays", "error");
return this;
},
}
(function(self) {
array.forEach(function(item, i) {
fn.call(self, self, item, i);
});
})(this);
return this;
};
/**
* Prints something to stdout.
*
* @param String text A string to echo to stdout
* @return Casper
*/
echo: function(text, style) {
console.log(style ? this.colorizer.colorize(text, style) : text);
return this;
},
/**
* Prints something to stdout.
*
* @param String text A string to echo to stdout
* @return Casper
*/
Casper.prototype.echo = function(text, style) {
console.log(style ? this.colorizer.colorize(text, style) : text);
return this;
};
/**
* Evaluates an expression in the page context, a bit like what
* WebPage#evaluate does, but the passed function can also accept
* parameters if a context Object is also passed:
*
* casper.evaluate(function(username, password) {
* document.querySelector('#username').value = username;
* document.querySelector('#password').value = password;
* document.querySelector('#submit').click();
* }, {
* username: 'Bazoonga',
* password: 'baz00nga'
* })
*
* FIXME: waiting for a patch of PhantomJS to allow direct passing of
* arguments to the function.
* TODO: don't forget to keep this backward compatible.
*
* @param Function fn The function to be evaluated within current page DOM
* @param Object context Object containing the parameters to inject into the function
* @return mixed
* @see WebPage#evaluate
*/
evaluate: function(fn, context) {
context = utils.isType(context, "object") ? context : {};
var newFn = require('injector').create(fn).process(context);
return this.page.evaluate(newFn);
},
/**
* Evaluates an expression in the page context, a bit like what
* WebPage#evaluate does, but the passed function can also accept
* parameters if a context Object is also passed:
*
* casper.evaluate(function(username, password) {
* document.querySelector('#username').value = username;
* document.querySelector('#password').value = password;
* document.querySelector('#submit').click();
* }, {
* username: 'Bazoonga',
* password: 'baz00nga'
* })
*
* FIXME: waiting for a patch of PhantomJS to allow direct passing of
* arguments to the function.
* TODO: don't forget to keep this backward compatible.
*
* @param Function fn The function to be evaluated within current page DOM
* @param Object context Object containing the parameters to inject into the function
* @return mixed
* @see WebPage#evaluate
*/
Casper.prototype.evaluate = function(fn, context) {
context = utils.isObject(context) ? context : {};
var newFn = require('injector').create(fn).process(context);
return this.page.evaluate(newFn);
};
/**
* Evaluates an expression within the current page DOM and die() if it
* returns false.
*
* @param function fn The expression to evaluate
* @param String message The error message to log
* @return Casper
*/
evaluateOrDie: function(fn, message) {
if (!this.evaluate(fn)) {
return this.die(message);
}
return this;
},
/**
* Evaluates an expression within the current page DOM and die() if it
* returns false.
*
* @param function fn The expression to evaluate
* @param String message The error message to log
* @return Casper
*/
Casper.prototype.evaluateOrDie = function(fn, message) {
if (!this.evaluate(fn)) {
return this.die(message);
}
return this;
};
/**
* Checks if an element matching the provided CSS3 selector exists in
* current page DOM.
*
* @param String selector A CSS3 selector
* @return Boolean
*/
exists: function(selector) {
return this.evaluate(function(selector) {
return __utils__.exists(selector);
}, { selector: selector });
},
/**
* Checks if an element matching the provided CSS3 selector exists in
* current page DOM.
*
* @param String selector A CSS3 selector
* @return Boolean
*/
Casper.prototype.exists = function(selector) {
return this.evaluate(function(selector) {
return __utils__.exists(selector);
}, { selector: selector });
};
/**
* Checks if an element matching the provided CSS3 selector is visible
* current page DOM by checking that offsetWidth and offsetHeight are
* both non-zero.
*
* @param String selector A CSS3 selector
* @return Boolean
*/
visible: function(selector) {
return this.evaluate(function(selector) {
return __utils__.visible(selector);
}, { selector: selector });
},
/**
* Checks if an element matching the provided CSS3 selector is visible
* current page DOM by checking that offsetWidth and offsetHeight are
* both non-zero.
*
* @param String selector A CSS3 selector
* @return Boolean
*/
Casper.prototype.visible = function(selector) {
return this.evaluate(function(selector) {
return __utils__.visible(selector);
}, { selector: selector });
};
/**
* Exits phantom.
*
* @param Number status Status
* @return Casper
*/
exit: function(status) {
phantom.exit(status);
return this;
},
/**
* Exits phantom.
*
* @param Number status Status
* @return Casper
*/
Casper.prototype.exit = function(status) {
this.emit('casper.exit', status);
phantom.exit(status);
return this;
};
/**
* Fetches innerText within the element(s) matching a given CSS3
* selector.
*
* @param String selector A CSS3 selector
* @return String
*/
fetchText: function(selector) {
return this.evaluate(function(selector) {
return __utils__.fetchText(selector);
}, { selector: selector });
},
/**
* Fetches innerText within the element(s) matching a given CSS3
* selector.
*
* @param String selector A CSS3 selector
* @return String
*/
Casper.prototype.fetchText = function(selector) {
return this.evaluate(function(selector) {
return __utils__.fetchText(selector);
}, { selector: selector });
};
/**
* Fills a form with provided field values.
*
* @param String selector A CSS3 selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
*/
fill: function(selector, vals, submit) {
submit = submit === true ? submit : false;
if (!utils.isType(selector, "string") || !selector.length) {
throw new Error("Form selector must be a non-empty string");
}
if (!utils.isType(vals, "object")) {
throw new Error("Form values must be provided as an object");
}
var fillResults = this.evaluate(function(selector, values) {
return __utils__.fill(selector, values);
}, {
selector: selector,
values: vals
});
if (!fillResults) {
throw new Error("Unable to fill form");
} else if (fillResults.errors.length > 0) {
(function(self){
fillResults.errors.forEach(function(error) {
self.log("form error: " + error, "error");
});
})(this);
if (submit) {
this.log("Errors encountered while filling form; submission aborted", "warning");
submit = false;
}
}
// File uploads
if (fillResults.files && fillResults.files.length > 0) {
(function(self) {
fillResults.files.forEach(function(file) {
var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
self.page.uploadFile(fileFieldSelector, file.path);
});
})(this);
}
// Form submission?
/**
* Fills a form with provided field values.
*
* @param String selector A CSS3 selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
*/
Casper.prototype.fill = function(selector, vals, submit) {
submit = submit === true ? submit : false;
if (!utils.isString(selector) || !selector.length) {
throw new Error("Form selector must be a non-empty string");
}
if (!utils.isObject(vals)) {
throw new Error("Form values must be provided as an object");
}
this.emit('casper.fill', selector, vals, submit);
var fillResults = this.evaluate(function(selector, values) {
return __utils__.fill(selector, values);
}, {
selector: selector,
values: vals
});
if (!fillResults) {
throw new Error("Unable to fill form");
} else if (fillResults.errors.length > 0) {
(function(self){
fillResults.errors.forEach(function(error) {
self.log("form error: " + error, "error");
});
})(this);
if (submit) {
this.evaluate(function(selector) {
var form = __utils__.findOne(selector);
var method = form.getAttribute('method').toUpperCase() || "GET";
var action = form.getAttribute('action') || "unknown";
__utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
form.submit();
}, { selector: selector });
this.log("Errors encountered while filling form; submission aborted", "warning");
submit = false;
}
},
/**
* Go a step forward in browser's history
*
* @return Casper
*/
forward: function(then) {
return this.then(function(self) {
self.evaluate(function() {
history.forward();
}
// File uploads
if (fillResults.files && fillResults.files.length > 0) {
(function(self) {
fillResults.files.forEach(function(file) {
var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
self.page.uploadFile(fileFieldSelector, file.path);
});
})(this);
}
// Form submission?
if (submit) {
this.evaluate(function(selector) {
var form = __utils__.findOne(selector);
var method = form.getAttribute('method').toUpperCase() || "GET";
var action = form.getAttribute('action') || "unknown";
__utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
form.submit();
}, { selector: selector });
}
};
/**
* Go a step forward in browser's history
*
* @return Casper
*/
Casper.prototype.forward = function(then) {
return this.then(function() {
this.emit('casper.forward');
this.evaluate(function() {
history.forward();
});
},
});
};
/**
* Retrieves current document url.
*
* @return String
*/
getCurrentUrl: function() {
return decodeURIComponent(this.evaluate(function() {
return document.location.href;
}));
},
/**
* Retrieves current document url.
*
* @return String
*/
Casper.prototype.getCurrentUrl = function() {
return decodeURIComponent(this.evaluate(function() {
return document.location.href;
}));
};
/**
* Retrieves boundaries for a DOM element matching the provided CSS3 selector.
*
* @param String selector A CSS3 selector
* @return Object
*/
getElementBounds: function(selector) {
if (!this.exists(selector)) {
throw new Error("No element matching selector found: " + selector);
}
var clipRect = this.evaluate(function(selector) {
return __utils__.getElementBounds(selector);
}, { selector: selector });
if (!utils.isClipRect(clipRect)) {
throw new Error('Could not fetch boundaries for element matching selector: ' + selector);
}
return clipRect;
},
/**
* Retrieves boundaries for a DOM element matching the provided CSS3 selector.
*
* @param String selector A CSS3 selector
* @return Object
*/
Casper.prototype.getElementBounds = function(selector) {
if (!this.exists(selector)) {
throw new Error("No element matching selector found: " + selector);
}
var clipRect = this.evaluate(function(selector) {
return __utils__.getElementBounds(selector);
}, { selector: selector });
if (!utils.isClipRect(clipRect)) {
throw new Error('Could not fetch boundaries for element matching selector: ' + selector);
}
return clipRect;
};
/**
* Retrieves global variable.
*
* @param String name The name of the global variable to retrieve
* @return mixed
*/
getGlobal: function(name) {
var result = this.evaluate(function(name) {
var result = {};
try {
result.value = JSON.stringify(window[name]);
} catch (e) {
var message = 'Unable to JSON encode window.' + name + ': ' + e;
__utils__.log(message, "error");
result.error = message;
}
return result;
}, {'name': name});
if ('error' in result) {
throw new Error(result.error);
} else if (utils.isType(result.value, "string")) {
return JSON.parse(result.value);
} else {
return undefined;
/**
* Retrieves global variable.
*
* @param String name The name of the global variable to retrieve
* @return mixed
*/
Casper.prototype.getGlobal = function(name) {
var result = this.evaluate(function(name) {
var result = {};
try {
result.value = JSON.stringify(window[name]);
} catch (e) {
var message = 'Unable to JSON encode window.' + name + ': ' + e;
__utils__.log(message, "error");
result.error = message;
}
},
return result;
}, {'name': name});
if ('error' in result) {
throw new Error(result.error);
} else if (utils.isString(result.value)) {
return JSON.parse(result.value);
} else {
return undefined;
}
};
/**
* Retrieves current page title, if any.
*
* @return String
*/
getTitle: function() {
return this.evaluate(function() {
return document.title;
});
},
/**
* Retrieves current page title, if any.
*
* @return String
*/
Casper.prototype.getTitle = function() {
return this.evaluate(function() {
return document.title;
});
};
/**
* Logs a message.
*
* @param String message The message to log
* @param String level The log message level (from Casper.logLevels property)
* @param String space Space from where the logged event occured (default: "phantom")
* @return Casper
*/
log: function(message, level, space) {
level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
space = space ? space : "phantom";
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)) {
return this; // skip logging
}
var entry = {
level: level,
space: space,
message: message,
date: new Date().toString()
};
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]);
message = levelStr + ' [' + space + '] ' + message;
}
if (this.options.verbose) {
this.echo(message); // direct output
}
this.result.log.push(entry);
return this;
},
/**
* Logs a message.
*
* @param String message The message to log
* @param String level The log message level (from Casper.logLevels property)
* @param String space Space from where the logged event occured (default: "phantom")
* @return Casper
*/
Casper.prototype.log = function(message, level, space) {
level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
space = space ? space : "phantom";
if (level === "error" && utils.isFunction(this.options.onError)) {
this.options.onError.call(this, this, message, space);
}
if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
return this; // skip logging
}
var entry = {
level: level,
space: space,
message: message,
date: new Date().toString()
};
if (level in this.logFormats && utils.isFunction(this.logFormats[level])) {
message = this.logFormats[level](message, level, space);
} else {
var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]);
message = levelStr + ' [' + space + '] ' + message;
}
if (this.options.verbose) {
this.echo(message); // direct output
}
this.result.log.push(entry);
this.emit('casper.log', entry);
return this;
};
/**
* Emulates a click on an HTML element matching a given CSS3 selector,
* using the mouse pointer.
*
* @param String selector A DOM CSS3 compatible selector
* @return Casper
*/
mouseClick: function(selector) {
this.mouse.click(selector);
return this;
},
/**
* Emulates a click on an HTML element matching a given CSS3 selector,
* using the mouse pointer.
*
* @param String selector A DOM CSS3 compatible selector
* @return Casper
*/
Casper.prototype.mouseClick = function(selector) {
this.mouse.click(selector);
return this;
};
/**
* Opens a page. Takes only one argument, the url to open (using the
* callback argument would defeat the whole purpose of Casper
* actually).
*
* @param String location The url to open
* @return Casper
*/
open: function(location, options) {
options = utils.isType(options, "object") ? options : {};
this.requestUrl = location;
// http auth
var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
if (httpAuthMatch) {
this.setHttpAuth(httpAuthMatch[1], httpAuthMatch[2]);
}
this.page.open(location);
return this;
},
/**
* Opens a page. Takes only one argument, the url to open (using the
* callback argument would defeat the whole purpose of Casper
* actually).
*
* @param String location The url to open
* @return Casper
*/
Casper.prototype.open = function(location, options) {
options = utils.isObject(options) ? options : {};
this.requestUrl = location;
// http auth
var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
if (httpAuthMatch) {
var httpAuth = {
username: httpAuthMatch[1],
password: httpAuthMatch[2]
};
this.emit('casper.http.auth', httpAuth);
this.setHttpAuth(httpAuth.username, httpAuth.password);
}
this.emit('casper.open', location);
this.page.open(location);
return this;
};
/**
* Repeats a step a given number of times.
*
* @param Number times Number of times to repeat step
* @aram function then The step closure
* @return Casper
* @see Casper#then
*/
repeat: function(times, then) {
for (var i = 0; i < times; i++) {
this.then(then);
}
return this;
},
/**
* Repeats a step a given number of times.
*
* @param Number times Number of times to repeat step
* @aram function then The step closure
* @return Casper
* @see Casper#then
*/
Casper.prototype.repeat = function(times, then) {
for (var i = 0; i < times; i++) {
this.then(then);
}
return this;
};
/**
* Checks if a given resource was loaded by the remote page.
*
* @param Function/String test A test function or string. In case a string is passed, url matching will be tested.
* @return Boolean
*/
resourceExists: function(test) {
var testFn;
if (utils.isType(test, "string")) {
testFn = function (res) {
return res.url.search(test) !== -1;
};
} else {
testFn = test;
}
return this.resources.some(testFn);
},
/**
* Checks if a given resource was loaded by the remote page.
*
* @param Function/String test A test function or string. In case a string is passed, url matching will be tested.
* @return Boolean
*/
Casper.prototype.resourceExists = function(test) {
var testFn;
if (utils.isString(test)) {
testFn = function (res) {
return res.url.search(test) !== -1;
};
} else {
testFn = test;
}
return this.resources.some(testFn);
};
/**
* Runs the whole suite of steps.
*
* @param function onComplete an optional callback
* @param Number time an optional amount of milliseconds for interval checking
* @return Casper
*/
run: function(onComplete, time) {
if (!this.steps || this.steps.length < 1) {
this.log("No steps defined, aborting", "error");
return this;
}
this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info");
this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete);
/**
* Runs the whole suite of steps.
*
* @param function onComplete an optional callback
* @param Number time an optional amount of milliseconds for interval checking
* @return Casper
*/
Casper.prototype.run = function(onComplete, time) {
if (!this.steps || this.steps.length < 1) {
this.log("No steps defined, aborting", "error");
return this;
},
}
this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info");
this.emit('casper.run');
this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete);
return this;
};
/**
* Runs a step.
*
* @param Function step
*/
runStep: function(step) {
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 (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 (utils.isType(self.options.onStepTimeout, "function")) {
self.options.onStepTimeout.call(self, self);
} else {
self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
}
/**
* Runs a step.
*
* @param Function step
*/
Casper.prototype.runStep = function(step) {
var skipLog = utils.isObject(step.options) && 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 (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) {
if (new Date().getTime() - start > self.options.stepTimeout) {
if (self.step == stepNum) {
self.emit('casper.step.timeout');
if (utils.isFunction(self.options.onStepTimeout)) {
self.options.onStepTimeout.call(self, self);
} else {
self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
}
clearInterval(stepTimeoutCheckInterval);
}
}, this.options.stepTimeout, this, new Date().getTime(), this.step);
}
try {
stepResult = step.call(this, this);
} catch (e) {
if (this.options.faultTolerant) {
this.log("Step error: " + e, "error");
} else {
throw e;
clearInterval(stepTimeoutCheckInterval);
}
}, this.options.stepTimeout, this, new Date().getTime(), this.step);
}
this.emit('casper.step.start', step);
try {
stepResult = step.call(this, this);
} catch (e) {
if (this.options.faultTolerant) {
this.log("Step error: " + e, "error");
} else {
throw e;
}
if (utils.isType(this.options.onStepComplete, "function")) {
this.options.onStepComplete.call(this, this, stepResult);
}
if (!skipLog) {
this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info");
}
},
}
if (utils.isFunction(this.options.onStepComplete)) {
this.options.onStepComplete.call(this, this, stepResult);
}
if (!skipLog) {
this.emit('casper.step.complete', stepResult);
this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info");
}
};
/**
* Sets HTTP authentication parameters.
*
* @param String username The HTTP_AUTH_USER value
* @param String password The HTTP_AUTH_PW value
* @return Casper
*/
setHttpAuth: function(username, password) {
if (!this.started) {
throw new Error("Casper must be started in order to use the setHttpAuth() method");
}
if (!utils.isType(username, "string") || !utils.isType(password, "string")) {
throw new Error("Both username and password must be strings");
}
this.page.settings.userName = username;
this.page.settings.password = password;
this.log("Setting HTTP authentication for user " + username, "info");
return this;
},
/**
* Sets HTTP authentication parameters.
*
* @param String username The HTTP_AUTH_USER value
* @param String password The HTTP_AUTH_PW value
* @return Casper
*/
Casper.prototype.setHttpAuth = function(username, password) {
if (!this.started) {
throw new Error("Casper must be started in order to use the setHttpAuth() method");
}
if (!utils.isString(username) || !utils.isString(password)) {
throw new Error("Both username and password must be strings");
}
this.page.settings.userName = username;
this.page.settings.password = password;
this.log("Setting HTTP authentication for user " + username, "info");
return this;
};
/**
* Configures and starts Casper.
*
* @param String location An optional location to open on start
* @param function then Next step function to execute on page loaded (optional)
* @return Casper
*/
start: function(location, then) {
this.log('Starting...', "info");
this.startTime = new Date().getTime();
this.history = [];
this.steps = [];
this.step = 0;
// Option checks
if (this.logLevels.indexOf(this.options.logLevel) < 0) {
this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning");
this.options.logLevel = "warning";
/**
* Configures and starts Casper.
*
* @param String location An optional location to open on start
* @param function then Next step function to execute on page loaded (optional)
* @return Casper
*/
Casper.prototype.start = function(location, then) {
this.emit('casper.starting');
this.log('Starting...', "info");
this.startTime = new Date().getTime();
this.history = [];
this.steps = [];
this.step = 0;
// Option checks
if (this.logLevels.indexOf(this.options.logLevel) < 0) {
this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning");
this.options.logLevel = "warning";
}
// WebPage
if (!utils.isWebPage(this.page)) {
if (utils.isWebPage(this.options.page)) {
this.page = this.options.page;
} else {
this.page = createPage(this);
}
// WebPage
if (!utils.isWebPage(this.page)) {
if (utils.isWebPage(this.options.page)) {
this.page = this.options.page;
}
this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings);
if (utils.isClipRect(this.options.clipRect)) {
this.page.clipRect = this.options.clipRect;
}
if (utils.isObject(this.options.viewportSize)) {
this.page.viewportSize = this.options.viewportSize;
}
this.started = true;
this.emit('casper.started');
if (utils.isNumber(this.options.timeout) && this.options.timeout > 0) {
this.log("Execution timeout set to " + this.options.timeout + 'ms', "info");
setTimeout(function(self) {
self.emit('casper.timeout');
if (utils.isFunction(self.options.onTimeout)) {
self.options.onTimeout.call(self, self);
} else {
this.page = createPage(this);
self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting.");
}
}
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 (utils.isType(this.options.viewportSize, "object")) {
this.page.viewportSize = this.options.viewportSize;
}
this.started = true;
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 (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 (utils.isType(this.options.onPageInitialized, "function")) {
this.log("Post-configuring WebPage instance", "debug");
this.options.onPageInitialized.call(this, this.page);
}
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");
}));
}
return this;
},
}, this.options.timeout, this);
}
casper.emit('casper.page.initialized');
if (utils.isFunction(this.options.onPageInitialized)) {
this.log("Post-configuring WebPage instance", "debug");
this.options.onPageInitialized.call(this, this.page);
}
if (utils.isString(location) && location.length > 0) {
return this.thenOpen(location, utils.isFunction(then) ? then : this.createStep(function(self) {
self.log("start page is loaded", "debug");
}));
}
return this;
};
/**
* Schedules the next step in the navigation process.
*
* @param function step A function to be called as a step
* @return Casper
*/
then: function(step) {
if (!this.started) {
throw new Error("Casper not started; please use Casper#start");
}
if (!utils.isType(step, "function")) {
throw new Error("You can only define a step as a function");
}
// check if casper is running
if (this.checker === null) {
// append step to the end of the queue
/**
* Schedules the next step in the navigation process.
*
* @param function step A function to be called as a step
* @return Casper
*/
Casper.prototype.then = function(step) {
if (!this.started) {
throw new Error("Casper not started; please use Casper#start");
}
if (!utils.isFunction(step)) {
throw new Error("You can only define a step as a function");
}
// check if casper is running
if (this.checker === null) {
// append step to the end of the queue
step.level = 0;
this.steps.push(step);
} else {
// insert substep a level deeper
try {
step.level = this.steps[this.step - 1].level + 1;
} catch (e) {
step.level = 0;
this.steps.push(step);
} else {
// insert substep a level deeper
try {
step.level = this.steps[this.step - 1].level + 1;
} catch (e) {
step.level = 0;
}
var insertIndex = this.step;
while (this.steps[insertIndex] && step.level === this.steps[insertIndex].level) {
insertIndex++;
}
this.steps.splice(insertIndex, 0, step);
}
return this;
},
var insertIndex = this.step;
while (this.steps[insertIndex] && step.level === this.steps[insertIndex].level) {
insertIndex++;
}
this.steps.splice(insertIndex, 0, step);
}
this.emit('casper.step.added', step);
return this;
};
/**
* Adds a new navigation step for clicking on a provided link selector
* and execute an optional next step.
*
* @param String selector A DOM CSS3 compatible selector
* @param Function then Next step function to execute on page loaded (optional)
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Casper
* @see Casper#click
* @see Casper#then
*/
thenClick: function(selector, then, fallbackToHref) {
this.then(function(self) {
self.click(selector, fallbackToHref);
});
return utils.isType(then, "function") ? this.then(then) : this;
},
/**
* Adds a new navigation step for clicking on a provided link selector
* and execute an optional next step.
*
* @param String selector A DOM CSS3 compatible selector
* @param Function then Next step function to execute on page loaded (optional)
* @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
* @return Casper
* @see Casper#click
* @see Casper#then
*/
Casper.prototype.thenClick = function(selector, then, fallbackToHref) {
this.then(function(self) {
self.click(selector, fallbackToHref);
});
return utils.isFunction(then) ? this.then(then) : this;
};
/**
* Adds a new navigation step to perform code evaluation within the
* current retrieved page DOM.
*
* @param function fn The function to be evaluated within current page DOM
* @param object context Optional function parameters context
* @return Casper
* @see Casper#evaluate
*/
thenEvaluate: function(fn, context) {
return this.then(function(self) {
self.evaluate(fn, context);
});
},
/**
* Adds a new navigation step to perform code evaluation within the
* current retrieved page DOM.
*
* @param function fn The function to be evaluated within current page DOM
* @param object context Optional function parameters context
* @return Casper
* @see Casper#evaluate
*/
Casper.prototype.thenEvaluate = function(fn, context) {
return this.then(function(self) {
self.evaluate(fn, context);
});
};
/**
* Adds a new navigation step for opening the provided location.
*
* @param String location The URL to load
* @param function then Next step function to execute on page loaded (optional)
* @return Casper
* @see Casper#open
*/
thenOpen: function(location, then) {
this.then(this.createStep(function(self) {
self.open(location);
}, {
skipLog: true
}));
return utils.isType(then, "function") ? this.then(then) : this;
},
/**
* Adds a new navigation step for opening the provided location.
*
* @param String location The URL to load
* @param function then Next step function to execute on page loaded (optional)
* @return Casper
* @see Casper#open
*/
Casper.prototype.thenOpen = function(location, then) {
this.then(this.createStep(function(self) {
self.open(location);
}, {
skipLog: true
}));
return utils.isFunction(then) ? this.then(then) : this;
};
/**
* Adds a new navigation step for opening and evaluate an expression
* against the DOM retrieved from the provided location.
*
* @param String location The url to open
* @param function fn The function to be evaluated within current page DOM
* @param object context Optional function parameters context
* @return Casper
* @see Casper#evaluate
* @see Casper#open
*/
thenOpenAndEvaluate: function(location, fn, context) {
return this.thenOpen(location).thenEvaluate(fn, context);
},
/**
* Adds a new navigation step for opening and evaluate an expression
* against the DOM retrieved from the provided location.
*
* @param String location The url to open
* @param function fn The function to be evaluated within current page DOM
* @param object context Optional function parameters context
* @return Casper
* @see Casper#evaluate
* @see Casper#open
*/
Casper.prototype.thenOpenAndEvaluate = function(location, fn, context) {
return this.thenOpen(location).thenEvaluate(fn, context);
};
/**
* Changes the current viewport size.
*
* @param Number width The viewport width, in pixels
* @param Number height The viewport height, in pixels
* @return Casper
*/
viewport: function(width, height) {
if (!this.started) {
throw new Error("Casper must be started in order to set viewport at runtime");
}
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 = {
width: width,
height: height
};
return this;
},
/**
* Changes the current viewport size.
*
* @param Number width The viewport width, in pixels
* @param Number height The viewport height, in pixels
* @return Casper
*/
Casper.prototype.viewport = function(width, height) {
if (!this.started) {
throw new Error("Casper must be started in order to set viewport at runtime");
}
if (!utils.isNumber(width) || !utils.isNumber(height) || width <= 0 || height <= 0) {
throw new Error("Invalid viewport width/height set: " + width + 'x' + height);
}
this.page.viewportSize = {
width: width,
height: height
};
this.emit('casper.viewport.changed', [width, height]);
return this;
};
/**
* Adds a new step that will wait for a given amount of time (expressed
* in milliseconds) before processing an optional next one.
*
* @param Number timeout The max amount of time to wait, in milliseconds
* @param Function then Next step to process (optional)
* @return Casper
*/
wait: function(timeout, then) {
timeout = Number(timeout, 10);
if (!utils.isType(timeout, "number") || timeout < 1) {
this.die("wait() only accepts a positive integer > 0 as a timeout value");
}
if (then && !utils.isType(then, "function")) {
this.die("wait() a step definition must be a function");
}
return this.then(function(self) {
self.waitStart();
setTimeout(function() {
self.log("wait() finished wating for " + timeout + "ms.", "info");
if (then) {
then.call(self, self);
}
self.waitDone();
}, timeout);
});
},
/**
* Adds a new step that will wait for a given amount of time (expressed
* in milliseconds) before processing an optional next one.
*
* @param Number timeout The max amount of time to wait, in milliseconds
* @param Function then Next step to process (optional)
* @return Casper
*/
Casper.prototype.wait = function(timeout, then) {
timeout = Number(timeout, 10);
if (!utils.isNumber(timeout) || timeout < 1) {
this.die("wait() only accepts a positive integer > 0 as a timeout value");
}
if (then && !utils.isFunction(then)) {
this.die("wait() a step definition must be a function");
}
return this.then(function(self) {
self.waitStart();
setTimeout(function() {
self.log("wait() finished wating for " + timeout + "ms.", "info");
if (then) {
then.call(self, self);
}
self.waitDone();
}, timeout);
});
};
waitStart: function() {
this.pendingWait = true;
},
Casper.prototype.waitStart = function() {
this.emit('casper.wait.start');
this.pendingWait = true;
};
waitDone: function() {
this.pendingWait = false;
},
Casper.prototype.waitDone = function() {
this.emit('casper.wait.done');
this.pendingWait = false;
};
/**
* Waits until a function returns true to process a next step.
*
* @param Function testFx A function to be evaluated for returning condition satisfecit
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitFor: function(testFx, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
if (!utils.isType(testFx, "function")) {
this.die("waitFor() needs a test function");
}
if (then && !utils.isType(then, "function")) {
this.die("waitFor() next step definition must be a function");
}
return this.then(function(self) {
self.waitStart();
var start = new Date().getTime();
var condition = false;
var interval = setInterval(function(self, testFx, timeout, onTimeout) {
if ((new Date().getTime() - start < timeout) && !condition) {
condition = testFx(self);
} else {
self.waitDone();
if (!condition) {
self.log("Casper.waitFor() timeout", "warning");
if (utils.isType(onTimeout, "function")) {
onTimeout.call(self, self);
} else {
self.die("Timeout of " + timeout + "ms expired, exiting.", "error");
}
clearInterval(interval);
/**
* Waits until a function returns true to process a next step.
*
* @param Function testFx A function to be evaluated for returning condition satisfecit
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitFor = function(testFx, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
if (!utils.isFunction(testFx)) {
this.die("waitFor() needs a test function");
}
if (then && !utils.isFunction(then)) {
this.die("waitFor() next step definition must be a function");
}
return this.then(function(self) {
self.waitStart();
var start = new Date().getTime();
var condition = false;
var interval = setInterval(function(self, testFx, timeout, onTimeout) {
if ((new Date().getTime() - start < timeout) && !condition) {
condition = testFx(self);
} else {
self.waitDone();
if (!condition) {
self.log("Casper.waitFor() timeout", "warning");
self.emit('casper.waitFor.timeout');
if (utils.isFunction(onTimeout)) {
onTimeout.call(self, self);
} else {
self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info");
if (then) {
self.then(then);
}
clearInterval(interval);
self.die("Timeout of " + timeout + "ms expired, exiting.", "error");
}
clearInterval(interval);
} else {
self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info");
if (then) {
self.then(then);
}
clearInterval(interval);
}
}, 100, self, testFx, timeout, onTimeout);
});
},
}
}, 100, self, testFx, timeout, onTimeout);
});
};
/**
* Waits until a given resource is loaded
*
* @param String/Function test A function to test if the resource exists. A string will be matched against the resources url.
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitForResource: function(test, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.resourceExists(test);
}, then, onTimeout, timeout);
},
/**
* Waits until a given resource is loaded
*
* @param String/Function test A function to test if the resource exists. A string will be matched against the resources url.
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitForResource = function(test, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.resourceExists(test);
}, then, onTimeout, timeout);
};
/**
* Waits until an element matching the provided CSS3 selector exists in
* remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitForSelector: function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.exists(selector);
}, then, onTimeout, timeout);
},
/**
* Waits until an element matching the provided CSS3 selector exists in
* remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitForSelector = function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.exists(selector);
}, then, onTimeout, timeout);
};
/**
* Waits until an element matching the provided CSS3 selector does not
* exist in the remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitWhileSelector: function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return !self.exists(selector);
}, then, onTimeout, timeout);
},
/**
* Waits until an element matching the provided CSS3 selector does not
* exist in the remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitWhileSelector = function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return !self.exists(selector);
}, then, onTimeout, timeout);
};
/**
* Waits until an element matching the provided CSS3 selector is
* visible in the remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitUntilVisible: function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.visible(selector);
}, then, onTimeout, timeout);
},
/**
* Waits until an element matching the provided CSS3 selector is
* visible in the remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitUntilVisible = function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return self.visible(selector);
}, then, onTimeout, timeout);
};
/**
* Waits until an element matching the provided CSS3 selector is no
* longer visible in remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
waitWhileVisible: function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return !self.visible(selector);
}, then, onTimeout, timeout);
}
/**
* Waits until an element matching the provided CSS3 selector is no
* longer visible in remote DOM to process a next step.
*
* @param String selector A CSS3 selector
* @param Function then The next step to perform (optional)
* @param Function onTimeout A callback function to call on timeout (optional)
* @param Number timeout The max amount of time to wait, in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitWhileVisible = function(selector, then, onTimeout, timeout) {
timeout = timeout ? timeout : this.defaultWaitTimeout;
return this.waitFor(function(self) {
return !self.visible(selector);
}, then, onTimeout, timeout);
};
/**
......@@ -1098,7 +1124,7 @@ Casper.prototype = {
* @param Object proto Prototype methods to add to Casper
*/
Casper.extend = function(proto) {
if (!utils.isType(proto, "object")) {
if (!utils.isObject(proto)) {
throw new Error("extends() only accept objects as prototypes");
}
utils.mergeObjects(Casper.prototype, proto);
......@@ -1114,14 +1140,15 @@ exports.Casper = Casper;
*/
function createPage(casper) {
var page;
if (phantom.version.major <= 1 && phantom.version.minor < 3 && utils.isType(require, "function")) {
if (phantom.version.major <= 1 && phantom.version.minor < 3 && utils.isFunction(require)) {
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.emit('casper.remote.alert', message);
if (utils.isFunction(casper.options.onAlert)) {
casper.options.onAlert.call(casper, casper, message);
}
};
......@@ -1132,25 +1159,32 @@ function createPage(casper) {
msg = test[2];
}
casper.log(msg, level, "remote");
casper.emit('casper.remote.message', msg);
};
page.onLoadStarted = function() {
casper.resources = [];
casper.loadInProgress = true;
casper.resources = [];
casper.emit('casper.load.started');
};
page.onLoadFinished = function(status) {
if (status !== "success") {
casper.emit('casper.load.failed', {
status: status,
http_status: casper.currentHTTPStatus,
url: casper.requestUrl
});
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")) {
if (utils.isFunction(casper.options.onLoadError)) {
casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
}
}
if (casper.options.clientScripts) {
if (!utils.isType(casper.options.clientScripts, "array")) {
if (!utils.isArray(casper.options.clientScripts)) {
casper.log("The clientScripts option must be an array", "error");
} else {
for (var i = 0; i < casper.options.clientScripts.length; i++) {
......@@ -1172,10 +1206,12 @@ function createPage(casper) {
}
// history
casper.history.push(casper.getCurrentUrl());
casper.emit('casper.load.finished', status);
casper.loadInProgress = false;
};
page.onResourceReceived = function(resource) {
if (utils.isType(casper.options.onResourceReceived, "function")) {
casper.emit('casper.resource.received', resource);
if (utils.isFunction(casper.options.onResourceReceived)) {
casper.options.onResourceReceived.call(casper, casper, resource);
}
if (resource.stage === "end") {
......@@ -1183,18 +1219,21 @@ function createPage(casper) {
}
if (resource.url === casper.requestUrl && resource.stage === "start") {
casper.currentHTTPStatus = resource.status;
if (utils.isType(casper.options.httpStatusHandlers, "object") &&
casper.emit('casper.http.status.' + resource.status, resource);
if (utils.isObject(casper.options.httpStatusHandlers) &&
resource.status in casper.options.httpStatusHandlers &&
utils.isType(casper.options.httpStatusHandlers[resource.status], "function")) {
utils.isFunction(casper.options.httpStatusHandlers[resource.status])) {
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.emit('casper.resource.requested', request);
if (utils.isFunction(casper.options.onResourceRequested)) {
casper.options.onResourceRequested.call(casper, casper, request);
}
};
casper.emit('casper.page.created', page);
return page;
}
......
......@@ -70,7 +70,7 @@ function fileExt(file) {
exports.fileExt = fileExt;
/**
* Takes a string and append blank until the pad value is reached.
* Takes a string and append blanks until the pad value is reached.
*
* @param String text
* @param Number pad Pad value (optional; default: 80)
......@@ -85,8 +85,14 @@ function fillBlanks(text, pad) {
}
exports.fillBlanks = fillBlanks;
/**
* Checks if value is a javascript Array
*
* @param mixed value
* @return Boolean
*/
function isArray(value) {
return isType(value, "array");
return Array.isArray(value) || isType(value, "array");
}
exports.isArray = isArray;
......@@ -101,17 +107,27 @@ function isCasperObject(value) {
}
exports.isCasperObject = isCasperObject;
/**
* Checks if value is a phantomjs clipRect-compatible object
*
* @param mixed value
* @return Boolean
*/
function isClipRect(value) {
return isType(value, "cliprect") || (
isType(value, "object") &&
isType(value.top, "number") &&
isType(value.left, "number") &&
isType(value.width, "number") &&
isType(value.height, "number")
isObject(value) &&
isNumber(value.top) && isNumber(value.left) &&
isNumber(value.width) && isNumber(value.height)
);
}
exports.isClipRect = isClipRect;
/**
* Checks if value is a javascript Function
*
* @param mixed value
* @return Boolean
*/
function isFunction(value) {
return isType(value, "function");
}
......@@ -125,15 +141,38 @@ exports.isFunction = isFunction;
*/
function isJsFile(file) {
var ext = fileExt(file);
return isType(ext, "string") && ['js', 'coffee'].indexOf(ext) !== -1;
return isString(ext, "string") && ['js', 'coffee'].indexOf(ext) !== -1;
}
exports.isJsFile = isJsFile;
/**
* Checks if value is a javascript Number
*
* @param mixed value
* @return Boolean
*/
function isNumber(value) {
return isType(value, "number");
}
exports.isNumber = isNumber;
/**
* Checks if value is a javascript Object
*
* @param mixed value
* @return Boolean
*/
function isObject(value) {
return isType(value, "object");
}
exports.isObject = isObject;
/**
* Checks if value is a javascript String
*
* @param mixed value
* @return Boolean
*/
function isString(value) {
return isType(value, "string");
}
......@@ -148,7 +187,10 @@ exports.isString = isString;
* @return Boolean
*/
function isType(what, typeName) {
return betterTypeOf(what) === typeName;
if (typeof typeName !== "string" || !typeName) {
throw new Error("You must pass isType() a typeName string");
}
return betterTypeOf(what).toLowerCase() === typeName.toLowerCase();
}
exports.isType = isType;
......@@ -159,10 +201,10 @@ exports.isType = isType;
* @return Boolean
*/
function isWebPage(what) {
if (!what || !isType(what, "object")) {
if (!what || !isObject(what)) {
return false;
}
if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
if (phantom.version.major <= 1 && phantom.version.minor < 3 && isFunction(require)) {
return what instanceof WebPage;
} else {
return what.toString().indexOf('WebPage(') === 0;
......@@ -202,9 +244,34 @@ exports.mergeObjects = mergeObjects;
function serialize(value) {
if (isType(value, "array")) {
value = value.map(function(prop) {
return isType(prop, "function") ? prop.toString().replace(/\s{2,}/, '') : prop;
return isFunction(prop) ? prop.toString().replace(/\s{2,}/, '') : prop;
});
}
return JSON.stringify(value, null, 4);
}
exports.serialize = serialize;
/**
* Inherit the prototype methods from one constructor into another.
*
* The Function.prototype.inherits from lang.js rewritten as a standalone
* function (not on Function.prototype). NOTE: If this file is to be loaded
* during bootstrapping this function needs to be revritten using some native
* functions as prototype setup using normal JavaScript does not work as
* expected during bootstrapping (see mirror.js in r114903).
*
* @param {function} ctor Constructor function which needs to inherit the
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
exports.inherits = function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
......