Commit 6e35765f 6e35765f79e3ef3c6b2ceced141d2d0b31cdebd3 by Nicolas Perriault

big tester refactor - see details in CHANGELOG

1 parent 1f7acddd
CasperJS Changelog
==================
XXXX-XX-XX, v1.1
----------------
This version is yet to be released.
### Important Changes & Caveats
#### Tester refactor
Scraping and testing are now betterly separated in CasperJS, and bad code is now a bit less bad. That involves breaking up BC on some points though:
- The Casper object won't be created with a `test` reference if not invoked using the [`casperjs test` command](http://casperjs.org/testing.html#casper-test-command), therefore the ability to run any test without calling it has been dropped. I know, get over it.
- Passing the planned number of tests to `casper.done()` has been dropped as well, because `done()` may be never called at all when big troubles happen; rather use the new `begin()` method and provide the expected number of tests using the second argument:
```js
casper.test.begin("Planning 4 tests", 4, function(test) {
[1, 2, 3, 4].forEach(function() {
test.assert(true);
});
test.done();
});
```
### Bugfixes & enhancements
None yet.
XXXX-XX-XX, v1.0.1
------------------
......
......@@ -151,13 +151,18 @@ var Casper = function Casper(options) {
this.started = false;
this.step = -1;
this.steps = [];
this.test = tester.create(this);
if (phantom.casperTest) {
this.test = tester.create(this);
}
// init phantomjs error handler
this.initErrorHandler();
this.on('error', function(msg, backtrace) {
if (msg === this.test.SKIP_MESSAGE) {
if (msg === this.test.SKIP_MESSAGE) { // FIXME: decouple testing
return;
}
if (msg.indexOf('AssertionError') === 0) { // FIXME: decouple testing
return;
}
var c = this.getColorizer();
......@@ -1303,16 +1308,23 @@ Casper.prototype.run = function run(onComplete, time) {
Casper.prototype.runStep = function runStep(step) {
"use strict";
this.checkStarted();
var skipLog = utils.isObject(step.options) && step.options.skipLog === true;
var stepInfo = f("Step %d/%d", this.step, this.steps.length);
var stepResult;
var skipLog = utils.isObject(step.options) && step.options.skipLog === true,
stepInfo = f("Step %d/%d", this.step, this.steps.length),
stepResult;
function getCurrentSuiteNum(casper) {
if (casper.test) {
return casper.test.currentSuiteNum + "-" + casper.step;
} else {
return casper.step;
}
}
if (!skipLog && /^http/.test(this.getCurrentUrl())) {
this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info");
}
if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
var stepTimeoutCheckInterval = setInterval(function _check(self, start, stepNum) {
if (new Date().getTime() - start > self.options.stepTimeout) {
if ((self.test.currentSuiteNum + "-" + self.step) === stepNum) {
if (getCurrentSuiteNum(self) === stepNum) {
self.emit('step.timeout');
if (utils.isFunction(self.options.onStepTimeout)) {
self.options.onStepTimeout.call(self, self.options.stepTimeout, stepNum);
......@@ -1320,10 +1332,15 @@ Casper.prototype.runStep = function runStep(step) {
}
clearInterval(stepTimeoutCheckInterval);
}
}, this.options.stepTimeout, this, new Date().getTime(), this.test.currentSuiteNum + "-" + this.step);
}, this.options.stepTimeout, this, new Date().getTime(), getCurrentSuiteNum(this));
}
this.emit('step.start', step);
stepResult = step.call(this, this.currentResponse);
try {
stepResult = step.call(this, this.currentResponse);
} catch (err) {
this.emit('step.error', err);
throw err;
}
if (utils.isFunction(this.options.onStepComplete)) {
this.options.onStepComplete.call(this, this, stepResult);
}
......@@ -1643,7 +1660,8 @@ Casper.prototype.visible = function visible(selector) {
};
/**
* Displays a warning message onto the console and logs the event.
* Displays a warning message onto the console and logs the event. Also emits a
* `warn` event with the message passed.
*
* @param String message
* @return Casper
......@@ -1652,6 +1670,7 @@ Casper.prototype.warn = function warn(message) {
"use strict";
this.log(message, "warning", "phantom");
var formatted = f.apply(null, ["⚠  " + message].concat([].slice.call(arguments, 1)));
this.emit('warn', message);
return this.echo(formatted, 'COMMENT');
};
......
......@@ -35,6 +35,15 @@ var events = require('events');
var utils = require('utils');
var f = utils.format;
function AssertionError(msg, result) {
Error.call(this);
this.message = msg;
this.name = 'AssertionError';
this.result = result;
}
AssertionError.prototype = new Error();
exports.AssertionError = AssertionError;
/**
* Creates a tester instance.
*
......@@ -79,10 +88,6 @@ var Tester = function Tester(casper, options) {
pre: [],
post: []
};
this.queue = [];
this.running = false;
this.started = false;
this.suiteResults = new TestSuiteResult();
this.options = utils.mergeObjects({
failFast: false, // terminates a suite as soon as a test fails?
failText: "FAIL", // text to use for a succesful test
......@@ -90,6 +95,10 @@ var Tester = function Tester(casper, options) {
pad: 80 , // maximum number of chars for a result line
warnText: "WARN" // text to use for a dubious test
}, options);
this.queue = [];
this.running = false;
this.started = false;
this.suiteResults = new TestSuiteResult();
this.configure();
......@@ -109,6 +118,12 @@ var Tester = function Tester(casper, options) {
if (failure.type) {
this.comment(' type: ' + failure.type);
}
if (failure.file) {
this.comment(' file: ' + failure.file + (failure.line ? ':' + failure.line : ''));
}
if (failure.lineContents) {
this.comment(' code: ' + failure.lineContents);
}
if (!failure.values || valueKeys.length === 0) {
return;
}
......@@ -119,7 +134,8 @@ var Tester = function Tester(casper, options) {
// casper events
this.casper.on('error', function onCasperError(msg, backtrace) {
if (!phantom.casperTest) {
var line = 0;
if (msg.indexOf('AssertionError') === 0) {
return;
}
if (msg === self.SKIP_MESSAGE) {
......@@ -127,19 +143,20 @@ var Tester = function Tester(casper, options) {
self.aborted = true;
return self.done();
}
var line = 0;
if (!utils.isString(msg)) {
try {
line = backtrace[0].line;
} catch (e) {}
}
try {
line = backtrace.filter(function(entry) {
return self.currentTestFile === entry.file;
})[0].line;
} catch (e) {}
self.uncaughtError(msg, self.currentTestFile, line, backtrace);
self.done();
});
this.casper.on('step.error', function onStepError(e) {
if (e.message !== self.SKIP_MESSAGE) {
self.uncaughtError(e, self.currentTestFile);
this.casper.on('step.error', function onStepError(error) {
if (error instanceof AssertionError) {
self.processAssertionError(error);
} else if (error.message !== self.SKIP_MESSAGE) {
self.uncaughtError(error, self.currentTestFile);
}
self.done();
});
......@@ -158,16 +175,19 @@ exports.Tester = Tester;
* family methods; supplementary informations are then passed using the
* `context` argument.
*
* Note: an AssertionError is thrown if the assertion fails.
*
* @param Boolean subject The condition to test
* @param String message Test description
* @param Object|null context Assertion context object (Optional)
* @return Object An assertion result object
* @return Object An assertion result object if test passed
* @throws AssertionError in case the test failed
*/
Tester.prototype.assert =
Tester.prototype.assertTrue = function assert(subject, message, context) {
"use strict";
this.executed++;
return this.processAssertionResult(utils.mergeObjects({
var result = utils.mergeObjects({
success: subject === true,
type: "assert",
standard: "Subject is strictly true",
......@@ -176,7 +196,11 @@ Tester.prototype.assertTrue = function assert(subject, message, context) {
values: {
subject: utils.getPropertyPath(context, 'values.subject') || subject
}
}, context || {}));
}, context || {});
if (!result.success) {
throw new AssertionError(message, result);
}
return this.processAssertionResult(result);
};
/**
......@@ -720,9 +744,10 @@ Tester.prototype.bar = function bar(text, style) {
* Starts a suite.
*
* @param String description Test suite description
* @param Number planned Number of planned tests in this suite
* @param Function suiteFn Suite function
*/
Tester.prototype.begin = function begin(description, suiteFn) {
Tester.prototype.begin = function begin(description, planned, suiteFn) {
"use strict";
if (this.started && this.running) {
return this.queue.push(arguments);
......@@ -731,14 +756,19 @@ Tester.prototype.begin = function begin(description, suiteFn) {
this.comment(description);
this.currentSuite = new TestCaseResult({
name: description,
file: this.currentTestFile
file: this.currentTestFile,
planned: Math.abs(~~planned) || undefined
});
this.executed = 0;
this.running = this.started = true;
try {
suiteFn.call(this, this, this.casper);
} catch (e) {
this.uncaughtError(e, this.currentTestFile, e.line);
} catch (err) {
if (err instanceof AssertionError) {
this.processAssertionError(err);
} else {
this.uncaughtError(err, this.currentTestFile, err.line);
}
this.done();
}
};
......@@ -796,7 +826,12 @@ Tester.prototype.configure = function configure() {
*/
Tester.prototype.done = function done(planned) {
"use strict";
if (planned > 0 && planned !== this.executed) {
if (arguments.length > 0) {
this.casper.warn('done() `planned` arg is deprecated as of 1.1');
}
if (this.currentSuite.planned && this.currentSuite.planned !== this.executed) {
this.dubious(this.currentSuite.planned, this.executed);
} else if (planned && planned !== this.executed) {
this.dubious(planned, this.executed);
}
if (this.currentSuite) {
......@@ -821,15 +856,15 @@ Tester.prototype.done = function done(planned) {
Tester.prototype.dubious = function dubious(planned, executed) {
"use strict";
var message = f('%d tests planned, %d tests executed', planned, executed);
return this.assert(false, message, {
type: "dubious",
standard: message,
message: message,
this.currentSuite.addWarning({
message: message,
file: this.currentTestFile,
values: {
planned: planned,
executed: executed
}
});
this.casper.warn(message);
};
/**
......@@ -938,6 +973,31 @@ Tester.prototype.pass = function pass(message) {
};
/**
* Processes an assertion error.
*
* @param AssertionError error
*/
Tester.prototype.processAssertionError = function(error) {
"use strict";
var result = error && error.result,
testFile = this.currentTestFile,
stackEntry;
try {
stackEntry = error.stackArray.filter(function(entry) {
return testFile === entry.sourceURL;
})[0];
} catch (e) {}
if (stackEntry) {
result.line = stackEntry.line;
try {
result.lineContents = fs.read(this.currentTestFile).split('\n')[result.line - 1].trim();
} catch (e) {
}
}
return this.processAssertionResult(result);
};
/**
* Processes an assertion result by emitting the appropriate event and
* printing result onto the console.
*
......@@ -947,9 +1007,11 @@ Tester.prototype.pass = function pass(message) {
Tester.prototype.processAssertionResult = function processAssertionResult(result) {
"use strict";
if (!this.currentSuite) {
// this is for BC when begin() didn't exist
this.currentSuite = new TestCaseResult({
name: "Untitled suite in " + this.currentTestFile,
file: this.currentTestFile
file: this.currentTestFile,
planned: undefined
});
}
var eventName = 'success',
......@@ -1001,7 +1063,6 @@ Tester.prototype.renderResults = function renderResults(exit, status, save) {
"use strict";
/*jshint maxstatements:20*/
save = save || this.options.save;
this.done(); // never too sure
var failed = this.suiteResults.countFailed(),
passed = this.suiteResults.countPassed(),
total = this.suiteResults.countTotal(),
......@@ -1269,16 +1330,52 @@ function TestCaseResult(options) {
"use strict";
this.name = options && options.name;
this.file = options && options.file;
this.assertions = 0;
this.passed = 0;
this.failed = 0;
this.passes = [];
this.planned = ~~(options && options.planned) || undefined;
this.errors = [];
this.failures = [];
this.passes = [];
this.warnings = [];
this.__defineGetter__("assertions", function() {
return this.passed + this.failed;
});
this.__defineGetter__("crashed", function() {
return this.errors.length;
});
this.__defineGetter__("failed", function() {
return this.failures.length;
});
this.__defineGetter__("passed", function() {
return this.passes.length;
});
}
exports.TestCaseResult = TestCaseResult;
/**
* Adds a success record and its execution time to their associated stacks.
* Adds a failure record and its execution time.
*
* @param Object failure
* @param Number time
*/
TestCaseResult.prototype.addFailure = function addFailure(failure, time) {
"use strict";
failure.suite = this.name;
failure.time = time;
this.failures.push(failure);
};
/**
* Adds an error record.
*
* @param Object failure
*/
TestCaseResult.prototype.addError = function addFailure(error) {
"use strict";
error.suite = this.name;
this.errors.push(error);
};
/**
* Adds a success record and its execution time.
*
* @param Object success
* @param Number time
......@@ -1288,23 +1385,16 @@ TestCaseResult.prototype.addSuccess = function addSuccess(success, time) {
success.suite = this.name;
success.time = time;
this.passes.push(success);
this.assertions++;
this.passed++;
};
/**
* Adds a failure record and its execution time to their associated stacks.
* Adds a warning record.
*
* @param Object failure
* @param Number time
* @param Object warning
*/
TestCaseResult.prototype.addFailure = function addFailure(failure, time) {
"use strict";
failure.suite = this.name;
failure.time = time;
this.failures.push(failure);
this.assertions++;
this.failed++;
TestCaseResult.prototype.addWarning = function addWarning(warning) {
warning.suite = this.name;
this.warnings.push(warning);
};
/**
......
......@@ -45,8 +45,7 @@ var TestSuiteResult = require('tester').TestSuiteResult;
*/
function generateClassName(classname) {
"use strict";
classname = classname.replace(phantom.casperPath, "").trim();
var script = classname || phantom.casperScript;
var script = classname.replace(phantom.casperPath, "").trim() || phantom.casperScript;
if (script.indexOf(fs.workingDirectory) === 0) {
script = script.substring(fs.workingDirectory.length + 1);
}
......@@ -56,12 +55,10 @@ function generateClassName(classname) {
if (~script.indexOf('.')) {
script = script.substring(0, script.lastIndexOf('.'));
}
// If we have trimmed our string down to nothing, default to script name
if (!script && phantom.casperScript) {
script = phantom.casperScript;
script = phantom.casperScript;
}
return script || "unknown";
}
......@@ -103,7 +100,7 @@ XUnitExporter.prototype.getXML = function getXML() {
var suiteNode = utils.node('testsuite', {
name: result.name,
tests: result.assertions,
failures: result.failed,
failures: result.failures.length,
time: utils.ms2seconds(result.calculateDuration()),
'package': generateClassName(result.file),
});
......
{
"name": "casperjs",
"description": "Navigation scripting & testing utility for PhantomJS",
"version": "1.0.0",
"version": "1.1.0-DEV",
"keywords": [
"phantomjs",
"javascript"
......
/*jshint strict:false*/
/*global CasperError console phantom require*/
/*global CasperError casper console phantom require*/
/**
* Google sample testing.
*
* Usage:
*
* $ casperjs test googletesting.js
*/
casper.test.begin('Google search retrieves 10 or more results', 5, function suite(test) {
casper.start("http://www.google.fr/", function() {
test.assertTitle("Google", "google homepage title is the one expected");
test.assertExists('form[action="/search"]', "main form is found");
this.fill('form[action="/search"]', {
q: "foo"
}, true);
});
var casper = require("casper").create({
logLevel: "debug"
});
casper.start("http://www.google.fr/", function() {
this.test.assertTitle("Google", "google homepage title is the one expected");
this.test.assertExists('form[action="/search"]', "main form is found");
this.fill('form[action="/search"]', {
q: "foo"
}, true);
});
casper.then(function() {
this.test.assertTitle("foo - Recherche Google", "google title is ok");
this.test.assertUrlMatch(/q=foo/, "search term has been submitted");
this.test.assertEval((function() {
return __utils__.findAll("h3.r").length >= 10;
}), "google search for \"foo\" retrieves 10 or more results");
});
casper.then(function() {
test.assertTitle("!!foo - Recherche Google", "google title is ok");
test.assertUrlMatch(/q=foo/, "search term has been submitted");
test.assertEval(function() {
return __utils__.findAll("h3.r").length >= 10;
}, "google search for \"foo\" retrieves 10 or more results");
});
casper.run(function() {
this.test.renderResults(true);
casper.run(function() {
test.done();
});
});
......
......@@ -2,7 +2,7 @@
/*global CasperError casper console phantom require*/
var fs = require('fs');
casper.test.begin('Tester.sortFiles()', function suite(test) {
casper.test.begin('Tester.sortFiles()', 1, function suite(test) {
var testDirRoot = fs.pathJoin(phantom.casperPath, 'tests', 'testdir');
var files = test.findTestFiles(testDirRoot);
var expected = [
......@@ -17,5 +17,5 @@ casper.test.begin('Tester.sortFiles()', function suite(test) {
return fs.pathJoin.apply(fs, [testDirRoot].concat(entry.split('/')));
});
test.assertEquals(files, expected, 'findTestFiles() find test files and sort them');
test.done(1);
test.done();
});
......
......@@ -3,17 +3,17 @@
var TestCaseResult = require('tester').TestCaseResult;
casper.test.begin('TestCaseResult.constructor() tests', function(test) {
casper.test.begin('TestCaseResult.constructor() tests', 4, function(test) {
var caseResult1 = new TestCaseResult();
test.assertType(caseResult1.name, "undefined", 'TestCaseResult.constructor() name is undefined by default');
test.assertType(caseResult1.file, "undefined", 'TestCaseResult.constructor() file is undefined by default');
var caseResult2 = new TestCaseResult({name: 'foo', file: '/tmp/foo'});
test.assertEquals(caseResult2.name, "foo", 'TestCaseResult.constructor() can set name');
test.assertEquals(caseResult2.file, "/tmp/foo", 'TestCaseResult.constructor() can set file');
test.done(4);
test.done();
});
casper.test.begin('TestCaseResult.addSuccess() and TestCaseResult.addFailure() tests', function(test) {
casper.test.begin('TestCaseResult.addSuccess() and TestCaseResult.addFailure() tests', 22, function(test) {
var caseResult = new TestCaseResult({name: 'foo', file: '/tmp/foo'});
test.assertEquals(caseResult.assertions, 0, 'test case result counts no assertion by default');
test.assertEquals(caseResult.passed, 0, 'test case result counts no success by default');
......@@ -43,8 +43,8 @@ casper.test.begin('TestCaseResult.addSuccess() and TestCaseResult.addFailure() t
caseResult.addSuccess({}, 1000);
test.assertEquals(caseResult.assertions, 3, 'test case result counts three assertions');
test.assertEquals(caseResult.passed, 2, 'test case result counts two successes');
test.assertEquals(caseResult.failed, 1, 'test case result counts no failure');
test.assertEquals(caseResult.failed, 1, 'test case result counts one failure');
test.assertEquals(caseResult.calculateDuration(), 1337 + 42 + 1000,
'TestCaseResult.calculateDuration() computes new tests duration');
test.done(22);
test.done();
});
......
......@@ -18,7 +18,7 @@ function generateCaseResult(options) {
return caseResult;
}
casper.test.begin('TestSuiteResult() basic tests', function(test) {
casper.test.begin('TestSuiteResult() basic tests', 8, function(test) {
var suiteResult = new TestSuiteResult();
test.assertEquals(suiteResult.constructor.name, 'Array', 'TestSuiteResult() is derived from Array');
test.assertEquals(suiteResult.countTotal(), 0);
......@@ -31,7 +31,7 @@ casper.test.begin('TestSuiteResult() basic tests', function(test) {
test.done();
});
casper.test.begin('TestSuiteResult() accumulation tests', function(test) {
casper.test.begin('TestSuiteResult() accumulation tests', 7, function(test) {
var suiteResult = new TestSuiteResult();
suiteResult.push(generateCaseResult({
name: 'foo',
......
......@@ -3,15 +3,15 @@
var tester = require('tester');
var testpage = require('webpage').create();
casper.test.begin('XUnitReporter() initialization', function suite() {
casper.test.begin('XUnitReporter() initialization', 1, function suite() {
var xunit = require('xunit').create();
var results = new tester.TestSuiteResult();
xunit.setResults(results);
this.assertTruthy(xunit.getXML());
this.done(1);
this.done();
});
casper.test.begin('XUnitReporter() can hold test suites', function suite() {
casper.test.begin('XUnitReporter() can hold test suites', 4, function suite() {
var xunit = require('xunit').create();
var results = new tester.TestSuiteResult();
var suite1 = new tester.TestCaseResult({
......@@ -32,10 +32,10 @@ casper.test.begin('XUnitReporter() can hold test suites', function suite() {
this.assertExists('testsuites[duration]');
this.assertExists('testsuite[name="foo"][package="foo"]');
this.assertExists('testsuite[name="bar"][package="bar"]');
this.done(4);
this.done();
});
casper.test.begin('XUnitReporter() can hold a suite with a succesful test', function suite() {
casper.test.begin('XUnitReporter() can hold a suite with a succesful test', 1, function suite() {
var xunit = require('xunit').create();
var results = new tester.TestSuiteResult();
var suite1 = new tester.TestCaseResult({
......@@ -52,10 +52,10 @@ casper.test.begin('XUnitReporter() can hold a suite with a succesful test', func
xunit.setResults(results);
casper.start().setContent(xunit.getXML());
this.assertExists('testsuite[name="foo"][package="foo"][tests="1"][failures="0"] testcase[name="footext"]');
casper.test.done(1);
casper.test.done();
});
casper.test.begin('XUnitReporter() can handle a failed test', function suite() {
casper.test.begin('XUnitReporter() can handle a failed test', 2, function suite() {
var xunit = require('xunit').create();
var results = new tester.TestSuiteResult();
var suite1 = new tester.TestCaseResult({
......@@ -73,5 +73,5 @@ casper.test.begin('XUnitReporter() can handle a failed test', function suite() {
casper.start().setContent(xunit.getXML());
this.assertExists('testsuite[name="foo"][package="foo"][tests="1"][failures="1"] testcase[name="footext"] failure[type="footype"]');
this.assertEquals(casper.getElementInfo('failure[type="footype"]').text, 'footext');
casper.test.done(2);
casper.test.done();
});
......