472 lines
12 KiB
JavaScript
472 lines
12 KiB
JavaScript
/*
|
|
JavaScript wrapper around REST API in Sweety.
|
|
*/
|
|
|
|
/**
|
|
* A convenience class for using XPath.
|
|
* @author Chris Corbyn
|
|
* @constructor
|
|
*/
|
|
function SweetyXpath() {
|
|
|
|
/**
|
|
* Get the first node matching the given expression.
|
|
* @param {String} expr
|
|
* @param {Element} node
|
|
* @returns Element
|
|
*/
|
|
this.getFirstNode = function getFirstNode(expr, node) {
|
|
var firstNode = _getRootNode(node).evaluate(
|
|
expr, node, _getNsResolver(node), XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
return firstNode.singleNodeValue;
|
|
},
|
|
|
|
/**
|
|
* Get all nodes matching the given expression.
|
|
* The returned result is a Node Snapshot.
|
|
* @param {String} expr
|
|
* @param {Element} node
|
|
* @returns Element[]
|
|
*/
|
|
this.getNodes = function getNodes(expr, node) {
|
|
var nodes = _getRootNode(node).evaluate(
|
|
expr, node, _getNsResolver(node), XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
|
|
var nodeSet = new Array();
|
|
for (var i = 0, len = nodes.snapshotLength; i < len; i++) {
|
|
nodeSet.push(nodes.snapshotItem(i));
|
|
}
|
|
return nodeSet;
|
|
},
|
|
|
|
/**
|
|
* Get the string value of the node matching the given expression.
|
|
* @param {String} expr
|
|
* @param {Element} node
|
|
* @returns String
|
|
*/
|
|
this.getValue = function getValue(expr, node) {
|
|
return _getRootNode(node).evaluate(
|
|
expr, node, _getNsResolver(node), XPathResult.STRING_TYPE, null).stringValue;
|
|
}
|
|
|
|
/**
|
|
* Get the root node from which run evaluate.
|
|
* @param {Element} node
|
|
* @returns Element
|
|
*/
|
|
var _getRootNode = function _getRootNode(node) {
|
|
if (node.ownerDocument && node.ownerDocument.evaluate) {
|
|
return node.ownerDocument;
|
|
} else {
|
|
if (node.evaluate) {
|
|
return node;
|
|
} else {
|
|
return document;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the NS Resolver used when searching.
|
|
* @param {Element} node
|
|
* @returns Element
|
|
*/
|
|
var _getNsResolver = function _getNsResolver(node) {
|
|
if (!document.createNSResolver) {
|
|
return null;
|
|
}
|
|
|
|
if (node.ownerDocument) {
|
|
return document.createNSResolver(node.ownerDocument.documentElement);
|
|
} else {
|
|
return document.createNSResolver(node.documentElement);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* The reporter interface so Sweety can tell the UI what's happening.
|
|
* @author Chris Corbyn
|
|
* @constructor
|
|
*/
|
|
function SweetyReporter() { //Interface/Base Class
|
|
|
|
var _this = this;
|
|
|
|
/**
|
|
* Create a sub-reporter for an individual test case.
|
|
* @param {String} testCaseName
|
|
* @returns SweetyReporter
|
|
*/
|
|
this.getReporterFor = function getReporterFor(testCaseName) {
|
|
return _this;
|
|
}
|
|
|
|
/**
|
|
* Start reporting.
|
|
*/
|
|
this.start = function start() {
|
|
}
|
|
|
|
/**
|
|
* Handle a skipped test case.
|
|
* @param {String} message
|
|
* @param {String} path
|
|
*/
|
|
this.reportSkip = function reportSkip(message, path) {
|
|
}
|
|
|
|
/**
|
|
* Handle a passing assertion.
|
|
* @param {String} message
|
|
* @param {String} path
|
|
*/
|
|
this.reportPass = function reportPass(message, path) {
|
|
}
|
|
|
|
/**
|
|
* Handle a failing assertion.
|
|
* @param {String} message
|
|
* @param {String} path
|
|
*/
|
|
this.reportFail = function reportFail(message, path) {
|
|
}
|
|
|
|
/**
|
|
* Handle an unexpected exception.
|
|
* @param {String} message
|
|
* @param {String} path
|
|
*/
|
|
this.reportException = function reportException(message, path) {
|
|
}
|
|
|
|
/**
|
|
* Handle miscellaneous test output.
|
|
* @param {String} output
|
|
* @param {String} path
|
|
*/
|
|
this.reportOutput = function reportOutput(output, path) {
|
|
}
|
|
|
|
/**
|
|
* Finish reporting.
|
|
*/
|
|
this.finish = function finish() {
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Represents a single test case being run.
|
|
* @author Chris Corbyn
|
|
* @constructor
|
|
*/
|
|
function SweetyTestCaseRun(testClass, reporter) {
|
|
|
|
var _this = this;
|
|
|
|
/** The XMLHttpRequest used in testing */
|
|
var _req;
|
|
|
|
/** XPath handler */
|
|
var _xpath = new SweetyXpath();
|
|
|
|
/** Callback function for completion event */
|
|
this.oncompletion = function oncompletion() {
|
|
}
|
|
|
|
/**
|
|
* Run this test.
|
|
*/
|
|
this.run = function run() {
|
|
if (!reporter.isStarted()) {
|
|
reporter.start();
|
|
}
|
|
_req = _createHttpRequest();
|
|
|
|
if (!_req) {
|
|
return;
|
|
}
|
|
|
|
_req.open("GET", "?test=" + testClass + "&format=xml", true);
|
|
_req.onreadystatechange = _handleXml;
|
|
_req.send(null);
|
|
}
|
|
|
|
/**
|
|
* Get an XmlHttpRequest instance, cross browser compatible.
|
|
* @return Object
|
|
*/
|
|
var _createHttpRequest = function _createHttpRequest() {
|
|
var req = false;
|
|
|
|
if (window.XMLHttpRequest && !(window.ActiveXObject)) {
|
|
try {
|
|
req = new XMLHttpRequest();
|
|
} catch(e) {
|
|
req = false;
|
|
}
|
|
} else if (window.ActiveXObject) {
|
|
try {
|
|
req = new ActiveXObject("Msxml2.XMLHTTP");
|
|
} catch(e) {
|
|
try {
|
|
req = new ActiveXObject("Microsoft.XMLHTTP");
|
|
} catch(e) {
|
|
req = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return req;
|
|
}
|
|
|
|
/**
|
|
* Handle the XML response from the test.
|
|
*/
|
|
var _handleXml = function _handleXml() {
|
|
if (_req.readyState == 4) {
|
|
try {
|
|
|
|
var xml = _req.responseXML;
|
|
var txt = _req.responseText.replace(/[\r\n]+/g, "").
|
|
replace(/^(.+)<\?xml.*$/, "$1");
|
|
|
|
//Test case was skipped
|
|
var skipElements = xml.getElementsByTagName('skip');
|
|
if (!skipElements || 1 != skipElements.length)
|
|
{
|
|
var runElements = xml.getElementsByTagName('run');
|
|
//Invalid document, an error probably occured
|
|
if (!runElements || 1 != runElements.length) {
|
|
reporter.reportException(
|
|
"Invalid XML response: " +
|
|
_stripTags(txt.replace(/^\s*<\?xml.+<\/(?:name|pass|fail|exception)>/g, "")), testClass);
|
|
} else {
|
|
var everything = runElements.item(0);
|
|
_parseResults(everything, testClass);
|
|
reporter.finish();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
reporter.reportSkip(_textValueOf(skipElements.item(0)), testClass);
|
|
reporter.finish();
|
|
}
|
|
} catch (ex) {
|
|
//Invalid document or an error occurred.
|
|
reporter.reportException(
|
|
"Invalid XML response: " +
|
|
_stripTags(txt.replace(/^\s*<\?xml.+<\/(?:name|pass|fail|exception)>/g, "")), testClass);
|
|
}
|
|
|
|
//Invoke the callback
|
|
_this.oncompletion();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cross browser method for reading the value of a node in XML.
|
|
* @param {Element} node
|
|
* @returns String
|
|
*/
|
|
var _textValueOf = function _textValueOf(node) {
|
|
if (!node.textContent && node.text) {
|
|
return node.text;
|
|
} else {
|
|
return node.textContent;
|
|
}
|
|
}
|
|
|
|
var _stripTags = function _stripTags(txt) {
|
|
txt = txt.replace(/[\r\n]+/g, "");
|
|
return txt.replace(
|
|
/<\/?(?:a|b|br|p|strong|u|i|em|span|div|ul|ol|li|table|thead|tbody|th|td|tr)\b.*?\/?>/g,
|
|
"");
|
|
}
|
|
|
|
/**
|
|
* Parse an arbitrary message output.
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseMessage = function _parseMessage(node, path) {
|
|
reporter.reportOutput(_textValueOf(node), path);
|
|
}
|
|
|
|
/**
|
|
* Parse formatted text output (such as a dump()).
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseFormatted = function _parseFormatted(node, path) {
|
|
reporter.reportOutput(_textValueOf(node), path);
|
|
}
|
|
|
|
/**
|
|
* Parse failing test assertion.
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseFail = function _parseFail(node, path) {
|
|
reporter.reportFail(_textValueOf(node), path);
|
|
}
|
|
|
|
/**
|
|
* Parse an Exception.
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseException = function _parseException(node, path) {
|
|
reporter.reportException(_textValueOf(node), path);
|
|
}
|
|
|
|
/**
|
|
* Parse passing test assertion.
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parsePass = function _parsePass(node, path) {
|
|
reporter.reportPass(_textValueOf(node), path);
|
|
}
|
|
|
|
/**
|
|
* Parse an entire test case
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseTestCase = function _parseTestCase(node, path) {
|
|
var testMethodNodes = _xpath.getNodes("./test", node);
|
|
|
|
for (var x in testMethodNodes) {
|
|
var testMethodNode = testMethodNodes[x];
|
|
var testMethodName = _xpath.getValue("./name", testMethodNode);
|
|
|
|
var formattedNodes = _xpath.getNodes("./formatted", testMethodNode);
|
|
for (var i in formattedNodes) {
|
|
var formattedNode = formattedNodes[i];
|
|
_parseFormatted(formattedNode, path + " -> " + testMethodName);
|
|
}
|
|
|
|
var messageNodes = _xpath.getNodes("./message", testMethodNode);
|
|
for (var i in messageNodes) {
|
|
var messageNode = messageNodes[i];
|
|
_parseMessage(messageNode, path + " -> " + testMethodName);
|
|
}
|
|
|
|
var failNodes = _xpath.getNodes("./fail", testMethodNode);
|
|
for (var i in failNodes) {
|
|
var failNode = failNodes[i];
|
|
_parseFail(failNode, path + " -> " + testMethodName);
|
|
}
|
|
|
|
var exceptionNodes = _xpath.getNodes("./exception", testMethodNode);
|
|
for (var i in exceptionNodes) {
|
|
var exceptionNode = exceptionNodes[i];
|
|
_parseException(exceptionNode, path + " -> " + testMethodName);
|
|
}
|
|
|
|
var passNodes = _xpath.getNodes("./pass", testMethodNode);
|
|
for (var i in passNodes) {
|
|
var passNode = passNodes[i];
|
|
_parsePass(passNode, path + " -> " + testMethodName);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse an entire grouped or single test case.
|
|
* @param {Element} node
|
|
* @param {String} path
|
|
*/
|
|
var _parseResults = function _parseResults(node, path) {
|
|
var groupNodes = _xpath.getNodes("./group", node);
|
|
|
|
if (0 != groupNodes.length) {
|
|
for (var i in groupNodes) {
|
|
var groupNode = groupNodes[i];
|
|
var groupName = _xpath.getValue("./name", groupNode);
|
|
_parseResults(groupNode, path + " -> " + groupName);
|
|
}
|
|
} else {
|
|
var caseNodes = _xpath.getNodes("./case", node);
|
|
for (var i in caseNodes) {
|
|
var caseNode = caseNodes[i];
|
|
_parseTestCase(caseNode, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Runs a list of test cases.
|
|
* @author Chris Corbyn
|
|
* @constructor
|
|
*/
|
|
function SweetyTestRunner() {
|
|
|
|
var _this = this;
|
|
|
|
SweetyTestRunner._currentInstance = _this;
|
|
|
|
/** True if the test runner has been stopped */
|
|
var _cancelled = false;
|
|
|
|
/**
|
|
* Invoked to cause the test runner to stop execution at the next available
|
|
* opportunity. If XML is being parsed in another thread, or an AJAX request
|
|
* is in progress the test runner will wait until the next test.
|
|
* @param {Boolean} cancel
|
|
*/
|
|
this.cancelTesting = function cancelTesting(cancel) {
|
|
_cancelled = cancel;
|
|
}
|
|
|
|
/**
|
|
* Run the given list of test cases.
|
|
* @param {String[]} tests
|
|
* @param {SweetyReporter} reporter
|
|
*/
|
|
this.runTests = function runTests(tests, reporter) {
|
|
if (!reporter.isStarted()) {
|
|
reporter.start();
|
|
}
|
|
|
|
if (_cancelled || !tests || !tests.length) {
|
|
_cancelled = false;
|
|
reporter.finish();
|
|
return;
|
|
}
|
|
|
|
var testCase = tests.shift();
|
|
|
|
var caseReporter = reporter.getReporterFor(testCase);
|
|
|
|
var testRun = new SweetyTestCaseRun(testCase, caseReporter);
|
|
|
|
//Repeat until no tests remaining in list
|
|
// Ok, I know, I know I'll try to eradicate this lazy use of recursion
|
|
testRun.oncompletion = function() {
|
|
_this.runTests(tests, reporter);
|
|
};
|
|
|
|
testRun.run();
|
|
}
|
|
|
|
}
|
|
|
|
/** Active instance */
|
|
SweetyTestRunner._currentInstance = null;
|
|
|
|
/**
|
|
* Fetches the currently running instance of the TestRunner.
|
|
* @returns SweetyTestRunner
|
|
*/
|
|
SweetyTestRunner.getCurrentInstance = function getCurrentInstance() {
|
|
return this._currentInstance;
|
|
}
|