Skip to content

Commit

Permalink
Allow Jasmine unit tests to be run from the Maven build
Browse files Browse the repository at this point in the history
It uses PhantomJS to execute SpecRunner.html and collects the results in target/surefire-reports. SpecRunner.html can also still be run directly from the browser as before.
Right now it's still a in a separate profile called 'jasmine-test'.
The integration uses jasmine.phantomjs-reporter.js, core.js and phantomjs_jasminexml_runner.js from the https://github.com/detro/phantomjs-jasminexml-example project. The author has given kind permission to use his code for other projects: https://github.com/detro/phantomjs-jasminexml-example/issues/2#issuecomment-10619232
  • Loading branch information
bosschaert committed Apr 3, 2013
1 parent ee2fbf1 commit 465c558
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 1 deletion.
32 changes: 32 additions & 0 deletions hawtio-web/pom.xml
Expand Up @@ -677,5 +677,37 @@
<jetty-use-file-lock>false</jetty-use-file-lock>
</properties>
</profile>

<profile>
<!-- Runs the Jasmine Unit tests, requires PhantonJS to be on the path -->
<id>jasmine-test</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin-version}</version>
<executions>
<execution>
<id>PhantomJS Unit Testing</id>
<phase>test</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>phantomjs</executable>
<workingDirectory>src/test/specs</workingDirectory>
<arguments>
<argument>phantomjs_jasminexml_runner.js</argument>
<argument>SpecRunner.html</argument>
<argument>${project.build.directory}/surefire-reports</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
4 changes: 3 additions & 1 deletion hawtio-web/src/test/specs/SpecRunner.html
Expand Up @@ -8,6 +8,7 @@
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-reporters/jasmine.phantomjs-reporter.js"></script>

<!-- include source files here... -->
<script type="text/javascript" src="../../main/webapp/lib/d3.v3.min.js"></script>
Expand Down Expand Up @@ -104,8 +105,9 @@
jasmineEnv.updateInterval = 1000;

var htmlReporter = new jasmine.HtmlReporter();

jasmineEnv.addReporter(htmlReporter);
var phantomReporter = new jasmine.PhantomJSReporter();
jasmineEnv.addReporter(phantomReporter);

jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
Expand Down
@@ -0,0 +1,204 @@
(function() {

if (! jasmine) {
throw new Exception("jasmine library does not exist in global namespace!");
}

function elapsed(startTime, endTime) {
return (endTime - startTime)/1000;
}

function ISODateString(d) {
function pad(n) { return n < 10 ? '0'+n : n; }

return d.getFullYear() + '-'
+ pad(d.getMonth()+1) +'-'
+ pad(d.getDate()) + 'T'
+ pad(d.getHours()) + ':'
+ pad(d.getMinutes()) + ':'
+ pad(d.getSeconds());
}

function trim(str) {
return str.replace(/^\s+/, "" ).replace(/\s+$/, "" );
}

function escapeInvalidXmlChars(str) {
return str.replace(/\&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/\>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/\'/g, "&apos;");
}

/**
* PhantomJS Reporter generates JUnit XML for the given spec run.
* Allows the test results to be used in java based CI.
* It appends some DOM elements/containers, so that a PhantomJS script can pick that up.
*
* @param {boolean} consolidate whether to save nested describes within the
* same file as their parent; default: true
* @param {boolean} useDotNotation whether to separate suite names with
* dots rather than spaces (ie "Class.init" not
* "Class init"); default: true
*/
var PhantomJSReporter = function(consolidate, useDotNotation) {
this.consolidate = consolidate === jasmine.undefined ? true : consolidate;
this.useDotNotation = useDotNotation === jasmine.undefined ? true : useDotNotation;
};

PhantomJSReporter.prototype = {
reportRunnerStarting: function(runner) {
this.log("Runner Started.");
},

reportSpecStarting: function(spec) {
spec.startTime = new Date();

if (! spec.suite.startTime) {
spec.suite.startTime = spec.startTime;
}

this.log(spec.suite.description + ' : ' + spec.description + ' ... ');
},

reportSpecResults: function(spec) {
var results = spec.results();
spec.didFail = !results.passed();
spec.status = spec.didFail ? 'Failed.' : 'Passed.';
if (results.skipped) {
spec.status = 'Skipped.';
}
this.log(spec.status);

spec.duration = elapsed(spec.startTime, new Date());
spec.output = '<testcase classname="' + this.getFullName(spec.suite) +
'" name="' + escapeInvalidXmlChars(spec.description) + '" time="' + spec.duration + '">';

var failure = "";
var failures = 0;
var resultItems = results.getItems();
for (var i = 0; i < resultItems.length; i++) {
var result = resultItems[i];

if (result.type == 'expect' && result.passed && !result.passed()) {
failures += 1;
failure += (failures + ": " + escapeInvalidXmlChars(result.message) + " ");
}
}
if (failure) {
spec.output += "<failure>" + trim(failure) + "</failure>";
}
spec.output += "</testcase>";
},

reportSuiteResults: function(suite) {
var results = suite.results();
var specs = suite.specs();
var specOutput = "";
// for JUnit results, let's only include directly failed tests (not nested suites')
var failedCount = 0;

suite.status = results.passed() ? 'Passed.' : 'Failed.';
suite.statusPassed = results.passed();
if (results.totalCount === 0) { // todo: change this to check results.skipped
suite.status = 'Skipped.';
}

// if a suite has no (active?) specs, reportSpecStarting is never called
// and thus the suite has no startTime -- account for that here
suite.startTime = suite.startTime || new Date();
suite.duration = elapsed(suite.startTime, new Date());

for (var i = 0; i < specs.length; i++) {
failedCount += specs[i].didFail ? 1 : 0;
specOutput += "\n " + specs[i].output;
}
suite.output = '\n<testsuite name="' + this.getFullName(suite) +
'" errors="0" tests="' + specs.length + '" failures="' + failedCount +
'" time="' + suite.duration + '" timestamp="' + ISODateString(suite.startTime) + '">';
suite.output += specOutput;
suite.output += "\n</testsuite>";
this.log(suite.description + ": " + results.passedCount + " of " + results.totalCount + " expectations passed.");
},

reportRunnerResults: function(runner) {
this.log("Runner Finished.");
var suites = runner.suites(),
passed = true;
for (var i = 0; i < suites.length; i++) {
var suite = suites[i],
filename = 'TEST-' + this.getFullName(suite, true) + '.xml',
output = '<?xml version="1.0" encoding="UTF-8" ?>';

passed = !suite.statusPassed ? false : passed;

// if we are consolidating, only write out top-level suites
if (this.consolidate && suite.parentSuite) {
continue;
}
else if (this.consolidate) {
output += "\n<testsuites>";
output += this.getNestedOutput(suite);
output += "\n</testsuites>";
this.createSuiteResultContainer(filename, output);
}
else {
output += suite.output;
this.createSuiteResultContainer(filename, output);
}
}
this.createTestFinishedContainer(passed);
},

getNestedOutput: function(suite) {
var output = suite.output;
for (var i = 0; i < suite.suites().length; i++) {
output += this.getNestedOutput(suite.suites()[i]);
}
return output;
},

createSuiteResultContainer: function(filename, xmloutput) {
jasmine.phantomjsXMLReporterResults = jasmine.phantomjsXMLReporterResults || [];
jasmine.phantomjsXMLReporterResults.push({
"xmlfilename" : filename,
"xmlbody" : xmloutput
});
},

createTestFinishedContainer: function(passed) {
jasmine.phantomjsXMLReporterPassed = passed
},

getFullName: function(suite, isFilename) {
var fullName;
if (this.useDotNotation) {
fullName = suite.description;
for (var parentSuite = suite.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
fullName = parentSuite.description + '.' + fullName;
}
}
else {
fullName = suite.getFullName();
}

// Either remove or escape invalid XML characters
if (isFilename) {
return fullName.replace(/[^\w]/g, "");
}
return escapeInvalidXmlChars(fullName);
},

log: function(str) {
var console = jasmine.getGlobal().console;

if (console && console.log) {
console.log(str);
}
}
};

// export public
jasmine.PhantomJSReporter = PhantomJSReporter;
})();
41 changes: 41 additions & 0 deletions hawtio-web/src/test/specs/lib/utils/core.js
@@ -0,0 +1,41 @@
/**
* Collection of Core JavaScript utility functionalities.
*/

// Namespace "utils.core"
var utils = utils || {};
utils.core = utils.core || {};

/**
* Wait until the test condition is true or a timeout occurs. Useful for waiting
* on a server response or for a ui change (fadeIn, etc.) to occur.
*
* @param check javascript condition that evaluates to a boolean.
* @param onTestPass what to do when 'check' condition is fulfilled.
* @param onTimeout what to do when 'check' condition is not fulfilled and 'timeoutMs' has passed
* @param timeoutMs the max amount of time to wait. Default value is 3 seconds
* @param freqMs how frequently to repeat 'check'. Default value is 250 milliseconds
*/
utils.core.waitfor = function(check, onTestPass, onTimeout, timeoutMs, freqMs) {
var timeoutMs = timeoutMs || 3000, //< Default Timeout is 3s
freqMs = freqMs || 250, //< Default Freq is 250ms
start = Date.now(),
condition = false,
timer = setTimeout(function() {
var elapsedMs = Date.now() - start;
if ((elapsedMs < timeoutMs) && !condition) {
// If not time-out yet and condition not yet fulfilled
condition = check(elapsedMs);
timer = setTimeout(arguments.callee, freqMs);
} else {
clearTimeout(timer); //< house keeping
if (!condition) {
// If condition still not fulfilled (timeout but condition is 'false')
onTimeout(elapsedMs);
} else {
// Condition fulfilled (timeout and/or condition is 'true')
onTestPass(elapsedMs);
}
}
}, freqMs);
};
61 changes: 61 additions & 0 deletions hawtio-web/src/test/specs/phantomjs_jasminexml_runner.js
@@ -0,0 +1,61 @@
// Based on this project https://github.com/detro/phantomjs-jasminexml-example
// Used with kind permission.
var htmlrunner,
resultdir,
page,
fs;

phantom.injectJs("lib/utils/core.js")

if ( phantom.args.length !== 2 ) {
console.log("Usage: phantom_test_runner.js HTML_RUNNER RESULT_DIR");
phantom.exit();
} else {
htmlrunner = phantom.args[0];
resultdir = phantom.args[1];
page = require("webpage").create();
fs = require("fs");

// Echo the output of the tests to the Standard Output
page.onConsoleMessage = function(msg, source, linenumber) {
console.log(msg);
};

page.open(htmlrunner, function(status) {
if (status === "success") {
utils.core.waitfor(function() { // wait for this to be true
return page.evaluate(function() {
return typeof(jasmine.phantomjsXMLReporterPassed) !== "undefined";
});
}, function() { // once done...
// Retrieve the result of the tests
var f = null, i, len;
suitesResults = page.evaluate(function(){
return jasmine.phantomjsXMLReporterResults;
});

// Save the result of the tests in files
for ( i = 0, len = suitesResults.length; i < len; ++i ) {
try {
f = fs.open(resultdir + '/' + suitesResults[i]["xmlfilename"], "w");
f.write(suitesResults[i]["xmlbody"]);
f.close();
} catch (e) {
console.log(e);
console.log("phantomjs> Unable to save result of Suite '"+ suitesResults[i]["xmlfilename"] +"'");
}
}

// Return the correct exit status. '0' only if all the tests passed
phantom.exit(page.evaluate(function(){
return jasmine.phantomjsXMLReporterPassed ? 0 : 1; //< exit(0) is success, exit(1) is failure
}));
}, function() { // or, once it timesout...
phantom.exit(1);
});
} else {
console.log("phantomjs> Could not load '" + htmlrunner + "'.");
phantom.exit(1);
}
});
}

0 comments on commit 465c558

Please sign in to comment.