Commit 324be9d1 324be9d1e66d6b49a61ea6c244ba23edd7735749 by Nicolas Perriault

enhanced stack traces, introduced the CasperError standard error, better display…

… of test suite results
1 parent be8f9f4c
......@@ -5,6 +5,7 @@ XXXX-XX-XX, v0.6.0
------------------
- **BC BREAK:** `Casper.click()` now uses native Webkit mouse events instead of previous crazy utopic javascript emulation
- **BC BREAK:** All errors thrown by CasperJS core are of the new `CasperError` type
- **BC BREAK:** removed obsolete `replaceFunctionPlaceholders()`
- *Deprecated*: `Casper.extend()` method has been deprecated; use natural javascript extension mechanisms instead (see samples)
- `Casper.open()` can now perform HTTP `GET`, `POST`, `PUT`, `DELETE` and `HEAD` operations
......@@ -13,6 +14,7 @@ XXXX-XX-XX, v0.6.0
- introduced the `mouse` module to handle native Webkit mouse events
- added support for `RegExp` input in `Casper.resourceExists()`
- added printing of source file path for any uncaught exception printed onto the console
- added an emulation of stack trace printing (but PhantomJS will have to upgrade its javascript engine for it to be fully working though)
---
......
......@@ -89,6 +89,53 @@ if (!phantom.casperLoaded) {
// Adding built-in capabilities to phantom object
phantom.sourceIds = {};
// custom global CasperError
window.CasperError = function(msg) {
Error.call(this);
try {
// let's get where this error has been thrown from, if we can
this._from = arguments.callee.caller.name;
} catch (e) {
this._from = "anonymous";
}
this.message = msg;
this.name = 'CasperError';
};
// standard Error prototype inheritance
window.CasperError.prototype = Object.getPrototypeOf(new Error());
// Stack formatting
window.CasperError.prototype.formatStack = function() {
var location;
if (this.fileName || this.sourceId) {
location = (this.fileName || phantom.sourceIds[this.sourceId]);
} else {
location = "unknown";
}
location += this.line ? ':' + this.line : 0;
return this.toString() + '\n ' + (this._from || "anonymous") + '() at ' + location;
};
// Adding stack traces to CasperError
// Inspired by phantomjs-nodify: https://github.com/jgonera/phantomjs-nodify/
// TODO: remove when phantomjs has js engine upgrade
if (!new CasperError().hasOwnProperty('stack')) {
Object.defineProperty(CasperError.prototype, 'stack', {
set: function(string) {
this._stack = string;
},
get: function() {
if (this._stack) {
return this._stack;
}
return this.formatStack();
},
configurable: true,
enumerable: true
});
}
phantom.getErrorMessage = function(e) {
return (e.fileName || this.sourceIds[e.sourceId]) + ':' + e.line + ' ' + e;
};
......@@ -190,30 +237,6 @@ if (!phantom.casperLoaded) {
};
})(require, phantom.casperPath);
// Adding stack traces to Error
// Inspired by phantomjs-nodify: https://github.com/jgonera/phantomjs-nodify/
// TODO: remove when phantomjs has js engine upgrade
if (!new Error().hasOwnProperty('stack')) {
Object.defineProperty(Error.prototype, 'stack', {
set: function(string) {
this._stack = string;
},
get: function() {
var location;
if (this._stack) {
return this._stack;
} else if (this.fileName || this.sourceId) {
location = phantom.getErrorMessage(this);
} else {
location = "unknown";
}
return this.toString() + '\n at ' + location;
},
configurable: true,
enumerable: true
});
}
// BC < 0.6
phantom.Casper = require('casper').Casper;
......
......@@ -166,7 +166,7 @@ Casper.prototype.capture = function(targetFile, clipRect) {
targetFile = fs.absolute(targetFile);
if (clipRect) {
if (!utils.isClipRect(clipRect)) {
throw new Error("clipRect must be a valid ClipRect object.");
throw new CasperError("clipRect must be a valid ClipRect object.");
}
previousClipRect = this.page.clipRect;
this.page.clipRect = clipRect;
......@@ -260,7 +260,7 @@ Casper.prototype.click = function(selector, fallbackToHref) {
*/
Casper.prototype.createStep = function(fn, options) {
if (!utils.isFunction(fn)) {
throw new Error("createStep(): a step definition must be a function");
throw new CasperError("createStep(): a step definition must be a function");
}
fn.options = utils.isObject(options) ? options : {};
this.emit('step.created', fn);
......@@ -467,10 +467,10 @@ Casper.prototype.fetchText = function(selector) {
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");
throw new CasperError("Form selector must be a non-empty string");
}
if (!utils.isObject(vals)) {
throw new Error("Form values must be provided as an object");
throw new CasperError("Form values must be provided as an object");
}
this.emit('fill', selector, vals, submit);
var fillResults = this.evaluate(function(selector, values) {
......@@ -480,7 +480,7 @@ Casper.prototype.fill = function(selector, vals, submit) {
values: vals
});
if (!fillResults) {
throw new Error("Unable to fill form");
throw new CasperError("Unable to fill form");
} else if (fillResults.errors.length > 0) {
(function(self){
fillResults.errors.forEach(function(error) {
......@@ -546,13 +546,13 @@ Casper.prototype.getCurrentUrl = function() {
*/
Casper.prototype.getElementBounds = function(selector) {
if (!this.exists(selector)) {
throw new Error("No element matching selector found: " + selector);
throw new CasperError("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);
throw new CasperError('Could not fetch boundaries for element matching selector: ' + selector);
}
return clipRect;
};
......@@ -576,7 +576,7 @@ Casper.prototype.getGlobal = function(name) {
return result;
}, {'name': name});
if ('error' in result) {
throw new Error(result.error);
throw new CasperError(result.error);
} else if (utils.isString(result.value)) {
return JSON.parse(result.value);
} else {
......@@ -662,20 +662,20 @@ Casper.prototype.open = function(location, settings) {
};
}
if (!utils.isObject(settings)) {
throw new Error("open(): request settings must be an Object");
throw new CasperError("open(): request settings must be an Object");
}
// http method
// taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302
var methods = ["get", "head", "put", "post", "delete"];
if (settings.method && (!utils.isString(settings.method) || methods.indexOf(settings.method) === -1)) {
throw new Error("open(): settings.method must be part of " + methods.join(', '));
throw new CasperError("open(): settings.method must be part of " + methods.join(', '));
}
// http data
if (settings.data) {
if (utils.isObject(settings.data)) { // query object
settings.data = qs.encode(settings.data);
} else if (!utils.isString(settings.data)) {
throw new Error("open(): invalid request settings data value: " + settings.data);
throw new CasperError("open(): invalid request settings data value: " + settings.data);
}
}
// current request url
......@@ -740,7 +740,7 @@ Casper.prototype.resourceExists = function(test) {
testFn = test;
break;
default:
throw new Error("Invalid type");
throw new CasperError("Invalid type");
}
return this.resources.some(testFn);
};
......@@ -819,10 +819,10 @@ Casper.prototype.runStep = function(step) {
*/
Casper.prototype.setHttpAuth = function(username, password) {
if (!this.started) {
throw new Error("Casper must be started in order to use the setHttpAuth() method");
throw new CasperError("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");
throw new CasperError("Both username and password must be strings");
}
this.page.settings.userName = username;
this.page.settings.password = password;
......@@ -899,10 +899,10 @@ Casper.prototype.start = function(location, then) {
*/
Casper.prototype.then = function(step) {
if (!this.started) {
throw new Error("Casper not started; please use Casper#start");
throw new CasperError("Casper not started; please use Casper#start");
}
if (!utils.isFunction(step)) {
throw new Error("You can only define a step as a function");
throw new CasperError("You can only define a step as a function");
}
// check if casper is running
if (this.checker === null) {
......@@ -1002,10 +1002,10 @@ Casper.prototype.thenOpenAndEvaluate = function(location, fn, context) {
*/
Casper.prototype.viewport = function(width, height) {
if (!this.started) {
throw new Error("Casper must be started in order to set viewport at runtime");
throw new CasperError("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(f("Invalid viewport: %dx%d", width, height));
throw new CasperError(f("Invalid viewport: %dx%d", width, height));
}
this.page.viewportSize = {
width: width,
......@@ -1194,7 +1194,7 @@ Casper.prototype.waitWhileVisible = function(selector, then, onTimeout, timeout)
Casper.extend = function(proto) {
console.warn('Casper.extend() has been deprecated since 0.6; check the docs');
if (!utils.isObject(proto)) {
throw new Error("extends() only accept objects as prototypes");
throw new CasperError("extends() only accept objects as prototypes");
}
utils.mergeObjects(Casper.prototype, proto);
};
......@@ -1254,7 +1254,7 @@ function createPage(casper) {
}
if (casper.options.clientScripts) {
if (!utils.isArray(casper.options.clientScripts)) {
throw new Error("The clientScripts option must be an array");
throw new CasperError("The clientScripts option must be an array");
} else {
casper.options.clientScripts.forEach(function(script) {
if (casper.page.injectJs(script)) {
......
......@@ -47,7 +47,7 @@ exports.parse = function(phantomArgs) {
} else if (typeof what === "string") {
return this.options[what];
} else {
throw new Error("Unsupported cli arg getter " + typeof what);
throw new CasperError("Unsupported cli arg getter " + typeof what);
}
}
};
......
......@@ -47,7 +47,7 @@ EventEmitter.prototype.emit = function() {
if (arguments[1] instanceof Error) {
throw arguments[1]; // Unhandled 'error' event
} else {
throw new Error("Uncaught, unspecified 'error' event.");
throw new CasperError("Uncaught, unspecified 'error' event.");
}
return false;
}
......@@ -98,7 +98,7 @@ EventEmitter.prototype.emit = function() {
// EventEmitter.prototype.emit() is also defined there.
EventEmitter.prototype.addListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('addListener only takes instances of Function');
throw new CasperError('addListener only takes instances of Function');
}
if (!this._events) this._events = {};
......@@ -145,7 +145,7 @@ EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('.once only takes instances of Function');
throw new CasperError('.once only takes instances of Function');
}
var self = this;
......@@ -162,7 +162,7 @@ EventEmitter.prototype.once = function(type, listener) {
EventEmitter.prototype.removeListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('removeListener only takes instances of Function');
throw new CasperError('removeListener only takes instances of Function');
}
// does not use listeners(), so no side effect of creating _events[type]
......@@ -231,7 +231,7 @@ EventEmitter.prototype.filter = function() {
EventEmitter.prototype.setFilter = function(type, filterFn) {
if (!this._filters) this._filters = {};
if ('function' !== typeof filterFn) {
throw new Error('setFilter only takes instances of Function');
throw new CasperError('setFilter only takes instances of Function');
}
if (!this._filters[type]) {
this._filters[type] = filterFn;
......
......@@ -41,7 +41,7 @@ exports.create = function(fn) {
*/
var FunctionArgsInjector = function(fn) {
if (!utils.isFunction(fn)) {
throw new Error("FunctionArgsInjector() can only process functions");
throw new CasperError("FunctionArgsInjector() can only process functions");
}
this.fn = fn;
......@@ -64,7 +64,7 @@ var FunctionArgsInjector = function(fn) {
this.process = function(values) {
var fnObj = this.extract(this.fn);
if (!utils.isObject(fnObj)) {
throw new Error("Unable to process function " + this.fn.toString());
throw new CasperError("Unable to process function " + this.fn.toString());
}
var inject = this.getArgsInjectionString(fnObj.args, values);
return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}';
......
......@@ -36,62 +36,62 @@ exports.create = function(casper) {
var Mouse = function(casper) {
if (!utils.isCasperObject(casper)) {
throw new Error('Mouse() needs a Casper instance');
throw new CasperError('Mouse() needs a Casper instance');
}
var supportedEvents = ['mouseup', 'mousedown', 'click', 'mousemove'];
var computeCenter = function(selector) {
function computeCenter(selector) {
var bounds = casper.getElementBounds(selector);
if (utils.isClipRect(bounds)) {
var x = Math.round(bounds.left + bounds.width / 2);
var y = Math.round(bounds.top + bounds.height / 2);
return [x, y];
}
};
}
var processEvent = function(type, args) {
function processEvent(type, args) {
if (!utils.isString(type) || supportedEvents.indexOf(type) === -1) {
throw new Error('Mouse.processEvent(): Unsupported mouse event type: ' + type);
throw new CasperError('Mouse.processEvent(): Unsupported mouse event type: ' + type);
}
args = Array.prototype.slice.call(args); // cast Arguments -> Array
casper.emit('mouse.' + type.replace('mouse', ''), args);
switch (args.length) {
case 0:
throw new Error('Mouse.processEvent(): Too few arguments');
throw new CasperError('Mouse.processEvent(): Too few arguments');
case 1:
// selector
var selector = args[0];
if (!utils.isString(selector)) {
throw new Error('Mouse.processEvent(): No valid CSS selector passed: ' + selector);
throw new CasperError('Mouse.processEvent(): No valid CSS selector passed: ' + selector);
}
casper.page.sendEvent.apply(casper.page, [type].concat(computeCenter(selector)));
break;
case 2:
// coordinates
if (!utils.isNumber(args[0]) || !utils.isNumber(args[1])) {
throw new Error('Mouse.processEvent(): No valid coordinates passed: ' + args);
throw new CasperError('Mouse.processEvent(): No valid coordinates passed: ' + args);
}
casper.page.sendEvent(type, args[0], args[1]);
break;
default:
throw new Error('Mouse.processEvent(): Too many arguments');
throw new CasperError('Mouse.processEvent(): Too many arguments');
}
};
}
this.click = function() {
this.click = function click() {
processEvent('click', arguments);
};
this.down = function() {
this.down = function down() {
processEvent('mousedown', arguments);
};
this.move = function() {
this.move = function move() {
processEvent('mousemove', arguments);
};
this.up = function() {
this.up = function up() {
processEvent('mouseup', arguments);
};
};
......
......@@ -31,6 +31,7 @@
var fs = require('fs');
var events = require('events');
var utils = require('utils');
var f = utils.format;
exports.create = function(casper, options) {
return new Tester(casper, options);
......@@ -47,7 +48,7 @@ var Tester = function(casper, options) {
this.options = utils.isObject(options) ? options : {};
if (!utils.isCasperObject(casper)) {
throw new Error("Tester needs a Casper instance");
throw new CasperError("Tester needs a Casper instance");
}
// locals
......@@ -309,7 +310,7 @@ var Tester = function(casper, options) {
this.exec = function(file) {
file = this.filter('exec.file', file) || file;
if (!fs.isFile(file) || !utils.isJsFile(file)) {
throw new Error("Can only exec() files with .js or .coffee extensions");
throw new CasperError("Can only exec() files with .js or .coffee extensions");
}
this.currentTestFile = file;
try {
......@@ -391,15 +392,20 @@ var Tester = function(casper, options) {
this.assert(true, message);
};
this.renderFailureDetails = function() {
if (this.testResults.failures.length === 0) {
/**
* Renders a detailed report for each failed test.
*
* @param Array failures
*/
this.renderFailureDetails = function(failures) {
if (failures.length === 0) {
return;
}
casper.echo("\nFailed test details\n");
this.testResults.failures.forEach(function(failure) {
casper.echo(f("\nDetails for the %d failed tests:\n", failures.length), "PARAMETER");
failures.forEach(function(failure) {
casper.echo('In ' + failure.file + ':');
var message;
if (utils.isType(failure.message, "error")) {
if (utils.isType(failure.message, "object") && failure.message.stack) {
message = failure.message.stack;
} else {
message = failure.message;
......@@ -432,7 +438,7 @@ var Tester = function(casper, options) {
}
casper.echo(this.colorize(utils.fillBlanks(result), style));
if (this.testResults.failed > 0) {
this.renderFailureDetails();
this.renderFailureDetails(this.testResults.failures);
}
if (save && utils.isFunction(require)) {
try {
......@@ -454,7 +460,7 @@ var Tester = function(casper, options) {
this.runSuites = function() {
var testFiles = [], self = this;
if (arguments.length === 0) {
throw new Error("No test suite to run");
throw new CasperError("No test suite to run");
}
Array.prototype.forEach.call(arguments, function(path) {
if (!fs.exists(path)) {
......
......@@ -268,7 +268,7 @@ exports.isString = isString;
*/
function isType(what, typeName) {
if (typeof typeName !== "string" || !typeName) {
throw new Error("You must pass isType() a typeName string");
throw new CasperError("You must pass isType() a typeName string");
}
return betterTypeOf(what).toLowerCase() === typeName.toLowerCase();
}
......
......@@ -33,12 +33,12 @@
this.test.assertEquals(results.testdown, [200, 100], 'Mouse.down() has pressed button to the specified position');
t.comment('Mouse.up()');
this.mouse.up(200, 100);
this.mouse.up(200, 200);
results = this.getGlobal('results');
this.test.assertEquals(results.testup, [200, 100], 'Mouse.up() has released button to the specified position');
t.comment('Mouse.move()');
this.mouse.move(200, 100);
this.mouse.move(200);
results = this.getGlobal('results');
this.test.assertEquals(results.testmove, [200, 100], 'Mouse.move() has moved to the specified position');
});
......