selenese-runner.js | |
---|---|
Script for running a collection of testsuitesThis is an utility for sending collections of selenese-test-suites to selenium-servers. The following is the code documentation. Usage documentation and tests are at: https://github.com/DBC-as/selenese-runner The code consists of two main parts:
The reporting function will be called for each event that
happens during the test. Events are JavaScript-objects, such as
The main function is asynchronous, which makes it easy to run several collections of testsuites in parallel, possibly talking with different selenium servers. | |
Dependencies | var soda = require('soda'); // selenium
var fs = require('fs'); // file system
var request = require('request'); // http(s) requests
var async = require('async'); // asynchronous utilities |
Main functionThe
| exports.runWithConfig = function(config) {
var basePath = config.suitelist.replace(/[^/]*$/, '');
var setup = Object.create(config.setup || {});
setup.url = setup.url || config.url; |
Connect to selenium (either in the cloud(sauce) or on internal server). | var browser;
if(setup['access-key']) {
browser = soda.createSauceClient(setup);
} else {
browser = soda.createClient(setup);
} |
Load the list of suites, | read(config.suitelist, function (err, data) {
if(!data || err) {
console.log(err, data); throw err;
} |
and start a new browser-session. | browser.session(function(err) {
if(err) {
return config.callback({
error: "Internal error, could not start browser",
err: err,
testDone: true});
} |
Remove comments in suitelist (comment lines start with #), and convert it to an array of names of suites. | var suiteNames = data
.replace(/#.*/g, '')
.split(/\s/)
.filter(function(e) {
return e !== '';
}); |
Then execute each of the suites.
| async.forEachSeries(suiteNames,
executeSuite,
function() {
browser.testComplete(function() {
config.callback({testDone: true});
});
});
});
}); |
Load, process and execute a testsuite | function executeSuite(suiteName, nextSuiteCallback) {
config.callback({testsuite: suiteName}); |
Load the testsuite. | read(basePath + suiteName, function(err, data) {
if(!data || err) {
console.log(err, data); throw err;
} |
If the suite is not a valid selenium-ide suite, report an error, and skip to next suite. | if(!data.match('<table id="suiteTable" cellpadding="1" ' +
'cellspacing="1" border="1" class="selenium">')) {
config.callback({
testcase: 'error-before-testcase-reached'});
config.callback({
error: "Testsuite doesn't look like a testsuite-" +
"file from selenium-IDE. Check that the " +
"url actually exists",
url: basePath + suiteName
});
return nextSuiteCallback();
} |
Extract the names of the tests from the testsuite. HACK: unfortunately | var tests = [];
data.replace(/<a href="([^"]*)">([^<]*)/g,
function(_,href,text) {
tests.push(href);
}); |
Path which the test filename is relative to. | suitePath = (basePath + suiteName).replace(/[^/]*$/, ''); |
Load, parse, | var testcaseAccumulator = {};
async.forEachSeries(tests,
function(elem, arrayCallback) {
parseTest(suitePath, elem, testcaseAccumulator,
arrayCallback, nextSuiteCallback);
}, |
preprocess, and execute the testcases. | function() {
prepareAndExecuteTests(testcaseAccumulator,
nextSuiteCallback);
});
});
} |
Load and parse a testcaseThe testcases will be stored in the testcaseAccumulator. | function parseTest(suitePath, test, testcaseAccumulator,
doneCallback, nextSuiteCallback) { |
Load the testcase. | read(suitePath + test, function(err, data) {
if(!data || err) {
console.log(err, data); throw err;
} |
The list of selenese-commands in the testcase. | var commands = []; |
Substitute target and value of each command
according to | var substitutions = config.replace || {};
function substitute(str) {
if(substitutions[str] === undefined) {
return str;
} else {
return substitutions[str];
}
} |
In selenese, multiple spaces are replaced with | function unescapeSelenese(str) {
return str.replace(/\xa0/g, ' ');
} |
Parse the test. HACK: unfortunately | data.replace(RegExp('<tr>\\s*<td>(.*?)<.td>\\s*' +
'<td>(.*?)<.td>\\s*' +
'<td>(.*?)<.td>\\s*<.tr>', 'g'),
function(_, command, target, value) { |
unescape it, and store the result in the list of commands. | commands.push({
command: command,
target: substitute(unescapeSelenese(
unescapexml(target))),
value: substitute(unescapeSelenese(
unescapexml(value)))
});
}); |
Add the test to the collection of tests. | testcaseAccumulator[test] = commands;
doneCallback();
});
} |
Preprocess testcases in a suite and execute themSupport for beforeEach and afterEach special test case which is pre-/appended to the other testcases | function prepareAndExecuteTests(testcases, nextSuiteCallback) {
var beforeEach = testcases.beforeEach || [];
delete testcases.beforeEach;
var afterEach = testcases.afterEach || [];
delete testcases.afterEach;
Object.keys(testcases).forEach(function(key) {
testcases[key] =
beforeEach.concat(testcases[key], afterEach);
}); |
Transform object to a list of objects, for easier accesss. Then execute the tests. | tests = Object.keys(testcases).map(function(key) {
return {name: key, commands: testcases[key] };
});
async.forEachSeries(tests,
function(elem, doneCallback) {
executeTestCase(elem, doneCallback, nextSuiteCallback);
}, nextSuiteCallback);
} |
Execute all commands in a single testcase | function executeTestCase(test, nextTestCallback,
nextSuiteCallback) {
config.callback({testcase: test.name});
async.forEachSeries(test.commands,
function(command, doneCallback) {
executeCommand(command, doneCallback,
nextTestCallback);
}, nextTestCallback);
} |
Execute a single selenium command | function executeCommand(command, doneCallback,
nextTestCallback) {
config.callback( {
info: "executing command",
command: command.command,
target: command.target,
value: command.value }); |
Handle a custom command: | if(command.command === 'restartBrowser') {
browser.testComplete(function() {
browser.session(function(err) {
if(err) {
return config.callback({
error: "Internal Error",
err: err,
testDone: true});
} |
and jump to the next selenese command when done. | doneCallback();
});
});
return; |
Handle unknown commands or | } else if(!browser[command.command]) {
config.callback({
error: 'Unknown command',
command: command.command,
target: command.target,
value: command.value
});
return nextTestCallback();
} |
and send the command to browser. | browser[command.command](command.target, command.value,
function(err, response, obj) { |
If sending the command fails, skip to next test. | if(err !== null) {
config.callback({
error: err,
command: command.command,
target: command.target,
value: command.value
});
return nextTestCallback();
} |
If the result of the command, is failure, signal an error. | if(response === 'false') {
config.callback({
error: "Command return false",
command: command.command,
target: command.target,
value: command.value });
} |
Continue with the next command. | doneCallback();
});
}
}; |
JUnit-compatible reporting callback | |
Static variablesSeveral junit-reporters can be active at once, - there will typically be one per testcollection run by | var junitReporters = 0;
var errorCount = 0; |
The reporting function generatorThe function itself create a new reporting function, which will write the testreport in a given | exports.junitReporter = (function(filename) {
++junitReporters; |
During the execution it keeps track of the current | var suite, testcase;
var results = {};
var errorDetected = false; |
Generate xml reportTransform to junit-like xml for Jenkins | function results2xml() {
var result = ['<testsuite name="root">\n'];
Object.keys(results).forEach(function(suite) {
result.push('<testsuite name="' + escapexml(suite) + '">');
Object.keys(results[suite]).forEach(function(testcase) {
result.push('<testcase name="' +
escapexml(testcase) + '">');
results[suite][testcase].forEach(function(err) {
result.push('<failure>' +
escapexml(JSON.stringify(err)) +
'</failure>');
});
result.push('</testcase>');
});
result.push('</testsuite>\n');
});
result.push('</testsuite>\n');
return result.join('');
} |
Callback function accumulating test results | return function(msg) {
msg.logfile = filename;
console.log(JSON.stringify(msg));
if(msg.testsuite) {
suite = msg.testsuite;
results[suite] = results[suite] || {};
}
if(msg.testcase) {
errorDetected = false;
testcase = msg.testcase;
results[suite][testcase] = results[suite][testcase] || [];
}
if(msg.error) {
results[suite][testcase].push(msg);
if(!errorDetected) {
errorCount++;
errorDetected = true;
}
} |
Exit with error code when all JunitReporters are done. | if(msg.testDone) {
fs.writeFile(filename, results2xml(), function() {
--junitReporters;
if(junitReporters === 0 ) {
process.exit(errorCount);
}
});
}
};
}); |
Utility codexml escape/unescapeTODO: extract to library | function escapexml(str) {
return str.replace(/[^ !#-;=?-~\n\r\t]/g, function(c) {
return '&#' + c.charCodeAt(0) + ';';
});
}
function unescapexml(str) {
return str.replace(/&([a-zA-Z0-9]*);/g, function(orig, entity) {
var entities = { gt: '>', lt: '<', nbsp: '\xa0' };
if(entities[entity]) {
return entities[entity];
}
throw({
error: 'Internal error, cannot convert entity',
entity: entity,
str: str
});
});
} |
Easy reading from url or fileTODO: extract to library | function read(filename, callback) {
if(filename.match(/^https?:\/\//i)) {
request(filename, function(err, response, data) {
callback(err,data);
});
} else {
fs.readFile(filename, 'utf-8', callback);
}
}
|