sweety.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /*
  2. JavaScript wrapper around REST API in Sweety.
  3. */
  4. /**
  5. * A convenience class for using XPath.
  6. * @author Chris Corbyn
  7. * @constructor
  8. */
  9. function SweetyXpath() {
  10. /**
  11. * Get the first node matching the given expression.
  12. * @param {String} expr
  13. * @param {Element} node
  14. * @returns Element
  15. */
  16. this.getFirstNode = function getFirstNode(expr, node) {
  17. var firstNode = _getRootNode(node).evaluate(
  18. expr, node, _getNsResolver(node), XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  19. return firstNode.singleNodeValue;
  20. },
  21. /**
  22. * Get all nodes matching the given expression.
  23. * The returned result is a Node Snapshot.
  24. * @param {String} expr
  25. * @param {Element} node
  26. * @returns Element[]
  27. */
  28. this.getNodes = function getNodes(expr, node) {
  29. var nodes = _getRootNode(node).evaluate(
  30. expr, node, _getNsResolver(node), XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  31. var nodeSet = new Array();
  32. for (var i = 0, len = nodes.snapshotLength; i < len; i++) {
  33. nodeSet.push(nodes.snapshotItem(i));
  34. }
  35. return nodeSet;
  36. },
  37. /**
  38. * Get the string value of the node matching the given expression.
  39. * @param {String} expr
  40. * @param {Element} node
  41. * @returns String
  42. */
  43. this.getValue = function getValue(expr, node) {
  44. return _getRootNode(node).evaluate(
  45. expr, node, _getNsResolver(node), XPathResult.STRING_TYPE, null).stringValue;
  46. }
  47. /**
  48. * Get the root node from which run evaluate.
  49. * @param {Element} node
  50. * @returns Element
  51. */
  52. var _getRootNode = function _getRootNode(node) {
  53. if (node.ownerDocument && node.ownerDocument.evaluate) {
  54. return node.ownerDocument;
  55. } else {
  56. if (node.evaluate) {
  57. return node;
  58. } else {
  59. return document;
  60. }
  61. }
  62. }
  63. /**
  64. * Get the NS Resolver used when searching.
  65. * @param {Element} node
  66. * @returns Element
  67. */
  68. var _getNsResolver = function _getNsResolver(node) {
  69. if (!document.createNSResolver) {
  70. return null;
  71. }
  72. if (node.ownerDocument) {
  73. return document.createNSResolver(node.ownerDocument.documentElement);
  74. } else {
  75. return document.createNSResolver(node.documentElement);
  76. }
  77. }
  78. }
  79. /**
  80. * The reporter interface so Sweety can tell the UI what's happening.
  81. * @author Chris Corbyn
  82. * @constructor
  83. */
  84. function SweetyReporter() { //Interface/Base Class
  85. var _this = this;
  86. /**
  87. * Create a sub-reporter for an individual test case.
  88. * @param {String} testCaseName
  89. * @returns SweetyReporter
  90. */
  91. this.getReporterFor = function getReporterFor(testCaseName) {
  92. return _this;
  93. }
  94. /**
  95. * Start reporting.
  96. */
  97. this.start = function start() {
  98. }
  99. /**
  100. * Handle a skipped test case.
  101. * @param {String} message
  102. * @param {String} path
  103. */
  104. this.reportSkip = function reportSkip(message, path) {
  105. }
  106. /**
  107. * Handle a passing assertion.
  108. * @param {String} message
  109. * @param {String} path
  110. */
  111. this.reportPass = function reportPass(message, path) {
  112. }
  113. /**
  114. * Handle a failing assertion.
  115. * @param {String} message
  116. * @param {String} path
  117. */
  118. this.reportFail = function reportFail(message, path) {
  119. }
  120. /**
  121. * Handle an unexpected exception.
  122. * @param {String} message
  123. * @param {String} path
  124. */
  125. this.reportException = function reportException(message, path) {
  126. }
  127. /**
  128. * Handle miscellaneous test output.
  129. * @param {String} output
  130. * @param {String} path
  131. */
  132. this.reportOutput = function reportOutput(output, path) {
  133. }
  134. /**
  135. * Finish reporting.
  136. */
  137. this.finish = function finish() {
  138. }
  139. }
  140. /**
  141. * Represents a single test case being run.
  142. * @author Chris Corbyn
  143. * @constructor
  144. */
  145. function SweetyTestCaseRun(testClass, reporter) {
  146. var _this = this;
  147. /** The XMLHttpRequest used in testing */
  148. var _req;
  149. /** XPath handler */
  150. var _xpath = new SweetyXpath();
  151. /** Callback function for completion event */
  152. this.oncompletion = function oncompletion() {
  153. }
  154. /**
  155. * Run this test.
  156. */
  157. this.run = function run() {
  158. if (!reporter.isStarted()) {
  159. reporter.start();
  160. }
  161. _req = _createHttpRequest();
  162. if (!_req) {
  163. return;
  164. }
  165. _req.open("GET", "?test=" + testClass + "&format=xml", true);
  166. _req.onreadystatechange = _handleXml;
  167. _req.send(null);
  168. }
  169. /**
  170. * Get an XmlHttpRequest instance, cross browser compatible.
  171. * @return Object
  172. */
  173. var _createHttpRequest = function _createHttpRequest() {
  174. var req = false;
  175. if (window.XMLHttpRequest && !(window.ActiveXObject)) {
  176. try {
  177. req = new XMLHttpRequest();
  178. } catch(e) {
  179. req = false;
  180. }
  181. } else if (window.ActiveXObject) {
  182. try {
  183. req = new ActiveXObject("Msxml2.XMLHTTP");
  184. } catch(e) {
  185. try {
  186. req = new ActiveXObject("Microsoft.XMLHTTP");
  187. } catch(e) {
  188. req = false;
  189. }
  190. }
  191. }
  192. return req;
  193. }
  194. /**
  195. * Handle the XML response from the test.
  196. */
  197. var _handleXml = function _handleXml() {
  198. if (_req.readyState == 4) {
  199. try {
  200. var xml = _req.responseXML;
  201. var txt = _req.responseText.replace(/[\r\n]+/g, "").
  202. replace(/^(.+)<\?xml.*$/, "$1");
  203. //Test case was skipped
  204. var skipElements = xml.getElementsByTagName('skip');
  205. if (!skipElements || 1 != skipElements.length)
  206. {
  207. var runElements = xml.getElementsByTagName('run');
  208. //Invalid document, an error probably occured
  209. if (!runElements || 1 != runElements.length) {
  210. reporter.reportException(
  211. "Invalid XML response: " +
  212. _stripTags(txt.replace(/^\s*<\?xml.+<\/(?:name|pass|fail|exception)>/g, "")), testClass);
  213. } else {
  214. var everything = runElements.item(0);
  215. _parseResults(everything, testClass);
  216. reporter.finish();
  217. }
  218. }
  219. else
  220. {
  221. reporter.reportSkip(_textValueOf(skipElements.item(0)), testClass);
  222. reporter.finish();
  223. }
  224. } catch (ex) {
  225. //Invalid document or an error occurred.
  226. reporter.reportException(
  227. "Invalid XML response: " +
  228. _stripTags(txt.replace(/^\s*<\?xml.+<\/(?:name|pass|fail|exception)>/g, "")), testClass);
  229. }
  230. //Invoke the callback
  231. _this.oncompletion();
  232. }
  233. }
  234. /**
  235. * Cross browser method for reading the value of a node in XML.
  236. * @param {Element} node
  237. * @returns String
  238. */
  239. var _textValueOf = function _textValueOf(node) {
  240. if (!node.textContent && node.text) {
  241. return node.text;
  242. } else {
  243. return node.textContent;
  244. }
  245. }
  246. var _stripTags = function _stripTags(txt) {
  247. txt = txt.replace(/[\r\n]+/g, "");
  248. return txt.replace(
  249. /<\/?(?:a|b|br|p|strong|u|i|em|span|div|ul|ol|li|table|thead|tbody|th|td|tr)\b.*?\/?>/g,
  250. "");
  251. }
  252. /**
  253. * Parse an arbitrary message output.
  254. * @param {Element} node
  255. * @param {String} path
  256. */
  257. var _parseMessage = function _parseMessage(node, path) {
  258. reporter.reportOutput(_textValueOf(node), path);
  259. }
  260. /**
  261. * Parse formatted text output (such as a dump()).
  262. * @param {Element} node
  263. * @param {String} path
  264. */
  265. var _parseFormatted = function _parseFormatted(node, path) {
  266. reporter.reportOutput(_textValueOf(node), path);
  267. }
  268. /**
  269. * Parse failing test assertion.
  270. * @param {Element} node
  271. * @param {String} path
  272. */
  273. var _parseFail = function _parseFail(node, path) {
  274. reporter.reportFail(_textValueOf(node), path);
  275. }
  276. /**
  277. * Parse an Exception.
  278. * @param {Element} node
  279. * @param {String} path
  280. */
  281. var _parseException = function _parseException(node, path) {
  282. reporter.reportException(_textValueOf(node), path);
  283. }
  284. /**
  285. * Parse passing test assertion.
  286. * @param {Element} node
  287. * @param {String} path
  288. */
  289. var _parsePass = function _parsePass(node, path) {
  290. reporter.reportPass(_textValueOf(node), path);
  291. }
  292. /**
  293. * Parse an entire test case
  294. * @param {Element} node
  295. * @param {String} path
  296. */
  297. var _parseTestCase = function _parseTestCase(node, path) {
  298. var testMethodNodes = _xpath.getNodes("./test", node);
  299. for (var x in testMethodNodes) {
  300. var testMethodNode = testMethodNodes[x];
  301. var testMethodName = _xpath.getValue("./name", testMethodNode);
  302. var formattedNodes = _xpath.getNodes("./formatted", testMethodNode);
  303. for (var i in formattedNodes) {
  304. var formattedNode = formattedNodes[i];
  305. _parseFormatted(formattedNode, path + " -> " + testMethodName);
  306. }
  307. var messageNodes = _xpath.getNodes("./message", testMethodNode);
  308. for (var i in messageNodes) {
  309. var messageNode = messageNodes[i];
  310. _parseMessage(messageNode, path + " -> " + testMethodName);
  311. }
  312. var failNodes = _xpath.getNodes("./fail", testMethodNode);
  313. for (var i in failNodes) {
  314. var failNode = failNodes[i];
  315. _parseFail(failNode, path + " -> " + testMethodName);
  316. }
  317. var exceptionNodes = _xpath.getNodes("./exception", testMethodNode);
  318. for (var i in exceptionNodes) {
  319. var exceptionNode = exceptionNodes[i];
  320. _parseException(exceptionNode, path + " -> " + testMethodName);
  321. }
  322. var passNodes = _xpath.getNodes("./pass", testMethodNode);
  323. for (var i in passNodes) {
  324. var passNode = passNodes[i];
  325. _parsePass(passNode, path + " -> " + testMethodName);
  326. }
  327. }
  328. }
  329. /**
  330. * Parse an entire grouped or single test case.
  331. * @param {Element} node
  332. * @param {String} path
  333. */
  334. var _parseResults = function _parseResults(node, path) {
  335. var groupNodes = _xpath.getNodes("./group", node);
  336. if (0 != groupNodes.length) {
  337. for (var i in groupNodes) {
  338. var groupNode = groupNodes[i];
  339. var groupName = _xpath.getValue("./name", groupNode);
  340. _parseResults(groupNode, path + " -> " + groupName);
  341. }
  342. } else {
  343. var caseNodes = _xpath.getNodes("./case", node);
  344. for (var i in caseNodes) {
  345. var caseNode = caseNodes[i];
  346. _parseTestCase(caseNode, path);
  347. }
  348. }
  349. }
  350. }
  351. /**
  352. * Runs a list of test cases.
  353. * @author Chris Corbyn
  354. * @constructor
  355. */
  356. function SweetyTestRunner() {
  357. var _this = this;
  358. SweetyTestRunner._currentInstance = _this;
  359. /** True if the test runner has been stopped */
  360. var _cancelled = false;
  361. /**
  362. * Invoked to cause the test runner to stop execution at the next available
  363. * opportunity. If XML is being parsed in another thread, or an AJAX request
  364. * is in progress the test runner will wait until the next test.
  365. * @param {Boolean} cancel
  366. */
  367. this.cancelTesting = function cancelTesting(cancel) {
  368. _cancelled = cancel;
  369. }
  370. /**
  371. * Run the given list of test cases.
  372. * @param {String[]} tests
  373. * @param {SweetyReporter} reporter
  374. */
  375. this.runTests = function runTests(tests, reporter) {
  376. if (!reporter.isStarted()) {
  377. reporter.start();
  378. }
  379. if (_cancelled || !tests || !tests.length) {
  380. _cancelled = false;
  381. reporter.finish();
  382. return;
  383. }
  384. var testCase = tests.shift();
  385. var caseReporter = reporter.getReporterFor(testCase);
  386. var testRun = new SweetyTestCaseRun(testCase, caseReporter);
  387. //Repeat until no tests remaining in list
  388. // Ok, I know, I know I'll try to eradicate this lazy use of recursion
  389. testRun.oncompletion = function() {
  390. _this.runTests(tests, reporter);
  391. };
  392. testRun.run();
  393. }
  394. }
  395. /** Active instance */
  396. SweetyTestRunner._currentInstance = null;
  397. /**
  398. * Fetches the currently running instance of the TestRunner.
  399. * @returns SweetyTestRunner
  400. */
  401. SweetyTestRunner.getCurrentInstance = function getCurrentInstance() {
  402. return this._currentInstance;
  403. }