testrunner.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. /*jshint evil:true */
  2. // Quick and dirty testrunner hack, it's ugly but it works
  3. (function(self) {
  4. function TestRunner() {
  5. var suites = [], suiteUrls = [], actions = {};
  6. var started, currentTest, testUrls = [], globalStats = {};
  7. var coverObjects = [];
  8. function get(id) {
  9. return document.getElementById(id);
  10. }
  11. function addClass(elm, cls) {
  12. if (cls && !hasClass(elm, cls)) {
  13. elm.className += elm.className ? ' ' + cls : cls;
  14. }
  15. }
  16. function removeClass(elm, cls) {
  17. if (hasClass(elm, cls)) {
  18. elm.className = elm.className.replace(new RegExp("(^|\\s+)" + cls + "(\\s+|$)", "g"), ' ');
  19. }
  20. }
  21. function hasClass(elm, cls) {
  22. return elm && cls && (' ' + elm.className + ' ').indexOf(' ' + cls + ' ') !== -1;
  23. }
  24. function init() {
  25. function loadNext() {
  26. var url = suiteUrls.shift();
  27. if (url) {
  28. loadSuite(url, function(json, url) {
  29. json.baseURL = url.substring(0, url.lastIndexOf('/'));
  30. if (json.baseURL) {
  31. json.baseURL += '/';
  32. }
  33. suites.push(json);
  34. loadNext();
  35. });
  36. } else {
  37. render();
  38. reflow();
  39. hashToStates();
  40. }
  41. }
  42. loadNext();
  43. }
  44. function getHashData() {
  45. var pos, hash = location.hash, items, item, data = {}, i;
  46. pos = hash.indexOf('!');
  47. if (pos > 0) {
  48. items = hash.substring(pos + 1).split('&');
  49. for (i = 0; i < items.length; i++) {
  50. item = items[i].split('=');
  51. data[item[0]] = item[1];
  52. }
  53. }
  54. return data;
  55. }
  56. function setHashData(data) {
  57. var name, hashItems = [];
  58. for (name in data) {
  59. if (data[name] !== null) {
  60. hashItems.push(name + '=' + data[name]);
  61. }
  62. }
  63. location.hash = '!' + hashItems.join('&');
  64. }
  65. function statesToHash() {
  66. var i, checkboxes, states = [], hasDisabled;
  67. checkboxes = get('tests').getElementsByTagName("input");
  68. for (i = 0; i < checkboxes.length; i++) {
  69. states[i] = checkboxes[i].checked ? '1' : '0';
  70. hasDisabled = hasDisabled || states[i] === '0';
  71. }
  72. setHashData({
  73. src: getSourceToLoad(),
  74. tests: hasDisabled ? states.join('') : null
  75. });
  76. }
  77. function hashToStates() {
  78. var i, data = getHashData(location.hash), checkboxes, source;
  79. source = data.src || 'min';
  80. get('source-' + source).checked = true;
  81. if (typeof(data.tests) != "undefined") {
  82. checkboxes = get('tests').getElementsByTagName("input");
  83. for (i = 0; i < checkboxes.length; i++) {
  84. checkboxes[i].checked = data.tests.substr(i, 1) === '1';
  85. }
  86. }
  87. }
  88. function addAction(name, action) {
  89. actions[name] = action;
  90. }
  91. function toggleCheckboxes(elm, state) {
  92. var checkboxes = (elm || get('tests')).getElementsByTagName("input"), i;
  93. for (i = 0; i < checkboxes.length; i++) {
  94. checkboxes[i].checked = state;
  95. }
  96. }
  97. function start() {
  98. var si, ti, tests;
  99. testUrls = [];
  100. for (si = 0; si < suites.length; si++) {
  101. tests = suites[si].tests;
  102. for (ti = 0; ti < tests.length; ti++) {
  103. if (get('c' + si + '-' + ti).checked) {
  104. testUrls.push(tests[ti]);
  105. }
  106. removeClass(get('t' + si + '-' + ti), "passed");
  107. removeClass(get('t' + si + '-' + ti), "failed");
  108. }
  109. }
  110. globalStats = {
  111. total: 0,
  112. failed: 0
  113. };
  114. // Start first test
  115. currentTest = testUrls.shift();
  116. if (currentTest) {
  117. get('testview').src = currentTest.url + "?src=" + getSourceToLoad();
  118. }
  119. get('coverage').disabled = true;
  120. }
  121. function stop() {
  122. started = false;
  123. get('testview').src = 'javascript:""';
  124. get('start').innerHTML = 'start';
  125. if (coverObjects.length) {
  126. get('coverage').disabled = false;
  127. }
  128. }
  129. addAction("start", function(elm) {
  130. started = !started;
  131. if (started) {
  132. start();
  133. } else {
  134. stop();
  135. reset();
  136. }
  137. elm.innerHTML = started ? 'stop' : 'start';
  138. });
  139. addAction("select-none", function(elm) {
  140. toggleCheckboxes(get('s' + elm.getAttribute("data-suite")), false);
  141. reset();
  142. });
  143. addAction("select-all", function(elm) {
  144. toggleCheckboxes(get('s' + elm.getAttribute("data-suite")), true);
  145. reset();
  146. });
  147. addAction("select-failed", function(elm) {
  148. var si, ti, tests;
  149. toggleCheckboxes(get('s' + elm.getAttribute("data-suite")), false);
  150. reset();
  151. var targetIndex = elm.getAttribute("data-suite");
  152. for (si = 0; si < suites.length; si++) {
  153. if (targetIndex !== null && targetIndex != si) {
  154. continue;
  155. }
  156. tests = suites[si].tests;
  157. for (ti = 0; ti < tests.length; ti++) {
  158. if (tests[ti].failed) {
  159. get('c' + si + '-' + ti).checked = true;
  160. }
  161. }
  162. }
  163. });
  164. addAction("coverage", function(elm) {
  165. if (elm.disabled) {
  166. return;
  167. }
  168. showCoverage();
  169. });
  170. function render() {
  171. var si, ti, tests, html = '';
  172. var div = document.createElement('div');
  173. addClass(div, "runner");
  174. html += '<div id="sidebar" class="sidebar" unselectable="true">';
  175. html += '<div id="controls" class="controls">';
  176. html += '<div>';
  177. html += '<button id="start" data-action="start">Start</button>';
  178. html += '<label><input id="source-min" name="source" type="radio" checked>Min</label>';
  179. html += '<label><input id="source-dev" name="source" type="radio">Dev</label>';
  180. html += '<label><input id="source-cov" name="source" type="radio">Cov</label>';
  181. html += '<button id="coverage" data-action="coverage" disabled>Coverage</button>';
  182. html += '</div>';
  183. html += '<div>';
  184. html += '<span id="gstatus" class="gstatus"></span>';
  185. html += 'Select: ';
  186. html += '<a data-action="select-all" href="javascript:;">[All]</a> ';
  187. html += '<a data-action="select-none" href="javascript:;">[None]</a> ';
  188. html += '<a data-action="select-failed" href="javascript:;">[Failed]</a>';
  189. html += '</div>';
  190. html += '</div>';
  191. html += '<div id="tests" class="tests">';
  192. for (si = 0; si < suites.length; si++) {
  193. tests = suites[si].tests;
  194. html += '<div id="s' + si + '" class="suite"><div class="suite-title">';
  195. html += '<div class="selection">';
  196. html += '<a data-action="select-all" data-suite="' + si + '" href="javascript:;">[All]</a> ';
  197. html += '<a data-action="select-none" data-suite="' + si + '" href="javascript:;">[None]</a> ';
  198. html += '<a data-action="select-failed" data-suite="' + si + '" href="javascript:;">[Failed]</a>';
  199. html += '</div>' + suites[si].title;
  200. html += '</div>';
  201. for (ti = 0; ti < tests.length; ti++) {
  202. tests[ti].suiteIndex = si;
  203. tests[ti].testIndex = ti;
  204. tests[ti].url = suites[si].baseURL + tests[ti].url;
  205. html += (
  206. '<div id="t' + si + '-' + ti + '" class="test">' +
  207. '<span id="s' + si + '-' + ti + '" class="stats">Running</span>' +
  208. '<input id="c' + si + '-' + ti + '" type="checkbox" checked />' +
  209. '<a href="' + tests[ti].url + '" target="testview" class="test-trigger">' + tests[ti].title + '</a>' +
  210. '</div>'
  211. );
  212. }
  213. html += '</div>';
  214. }
  215. html += '</div>';
  216. html += '</div>';
  217. html += '<iframe id="testview" name="testview" src="javascript:\'\'"></iframe>';
  218. // coverage
  219. html += '<div id="overlay"></div>';
  220. html += '<div id="coverview">';
  221. html += '<a class="close" href="javascript:TestRunner.hideCoverage();" title="Close">x</a>';
  222. html += '<iframe frameborder="0" src="javascript:\'\'"></iframe>';
  223. html += '</div>';
  224. div.innerHTML = html;
  225. document.body.appendChild(div);
  226. get('sidebar').onclick = function(e) {
  227. var target, action;
  228. e = e || event;
  229. target = e.target || e.srcElement;
  230. if ((action = actions[target.getAttribute("data-action")])) {
  231. action(target);
  232. } else if (target.className === 'test-trigger') {
  233. get(target.getAttribute('target')).src = target.href + '?src=' + getSourceToLoad();
  234. return false; // prevent default action
  235. }
  236. statesToHash();
  237. };
  238. }
  239. function addSuites(urls) {
  240. suiteUrls.push.apply(suiteUrls, urls);
  241. }
  242. function loadSuite(url, callback) {
  243. var xhr;
  244. function getNativeXHR() {
  245. if (window.XMLHttpRequest) {
  246. return new window.XMLHttpRequest();
  247. } else {
  248. return (function() {
  249. var progIDs = ['Msxml2.XMLHTTP.6.0', 'Microsoft.XMLHTTP']; // if 6.0 available, use it, otherwise failback to default 3.0
  250. for (var i = 0; i < progIDs.length; i++) {
  251. try {
  252. return new ActiveXObject(progIDs[i]);
  253. } catch (ex) {}
  254. }
  255. })();
  256. }
  257. }
  258. function ready() {
  259. if (xhr.readyState == 4) {
  260. callback(eval("(" + xhr.responseText + ")"), url);
  261. xhr = null;
  262. } else {
  263. setTimeout(ready, 10);
  264. }
  265. }
  266. xhr = getNativeXHR();
  267. if (xhr) {
  268. xhr.open('GET', url, true);
  269. xhr.send();
  270. setTimeout(ready, 10);
  271. }
  272. }
  273. function reflow() {
  274. var viewPortW, viewPortH, sideBarWidth, controlsHeight;
  275. function rect(id, x, y, w, h) {
  276. var style, elm;
  277. if ((elm = get(id))) {
  278. style = elm.style;
  279. style.left = x + "px";
  280. style.top = y + "px";
  281. style.width = w + "px";
  282. style.height = h + "px";
  283. }
  284. }
  285. viewPortW = window.innerWidth || document.documentElement.clientWidth;
  286. viewPortH = window.innerHeight || document.documentElement.clientHeight;
  287. sideBarWidth = 300;
  288. controlsHeight = 60;
  289. rect('testview', sideBarWidth, 0, viewPortW - sideBarWidth, viewPortH);
  290. rect('sidebar', 0, 0, sideBarWidth, viewPortH);
  291. rect('controls', 0, 0, sideBarWidth, controlsHeight);
  292. rect('tests', 0, controlsHeight, sideBarWidth, viewPortH - controlsHeight);
  293. }
  294. function reset() {
  295. var si, tests, ti;
  296. stop();
  297. get('gstatus').innerHTML = '';
  298. removeClass(get("controls"), "failed");
  299. for (si = 0; si < suites.length; si++) {
  300. tests = suites[si].tests;
  301. for (ti = 0; ti < tests.length; ti++) {
  302. removeClass(get('t' + si + '-' + ti), "passed");
  303. removeClass(get('t' + si + '-' + ti), "failed");
  304. removeClass(get('t' + si + '-' + ti), "running");
  305. }
  306. }
  307. }
  308. function getSourceToLoad() {
  309. var radios, source = 'min';
  310. radios = document.getElementsByName("source");
  311. for (var i = 0; i < radios.length; i++) {
  312. if (radios[i].checked) {
  313. source = radios[i].id.replace(/^source-/, '');
  314. break;
  315. }
  316. }
  317. return source;
  318. }
  319. function updateGlobalStatus() {
  320. get('gstatus').innerHTML = 'Total: ' + globalStats.total + ", Failed: " + globalStats.failed;
  321. addClass(get("controls"), globalStats.failed > 0 ? "failed" : "");
  322. }
  323. function done(failed, total) {
  324. var nextTest, currentTestElm;
  325. function runNextTest() {
  326. if ((nextTest = testUrls.shift())) {
  327. currentTest = nextTest;
  328. currentTestElm = get('t' + currentTest.suiteIndex + '-' + currentTest.testIndex);
  329. currentTestElm.scrollIntoView(false);
  330. addClass(currentTestElm, "running");
  331. get('testview').src = nextTest.url + "?src=" + getSourceToLoad();
  332. } else {
  333. stop();
  334. }
  335. }
  336. if (started) {
  337. currentTest.failed = failed;
  338. currentTest.total = total;
  339. globalStats.total += total;
  340. globalStats.failed += failed;
  341. updateGlobalStatus();
  342. get('s' + currentTest.suiteIndex + '-' + currentTest.testIndex).innerHTML = (
  343. '(<span class="failed">' + failed + '</span>, ' +
  344. '<span class="passed">' + (total - failed) + '</span>, ' +
  345. '<span class="total">' + total + '</span>)'
  346. );
  347. addClass(get('t' + currentTest.suiteIndex + '-' + currentTest.testIndex), failed > 0 ? 'failed' : 'passed');
  348. removeClass(currentTestElm, "running");
  349. runNextTest();
  350. }
  351. }
  352. function addCoverObject(coverObject) {
  353. coverObjects.push(coverObject);
  354. }
  355. // this is going to be called from the coverage iframe
  356. function getCoverObject() {
  357. var coverObject = {}, fileName, gaps, gap, count;
  358. for (var i = 0, length = coverObjects.length; i < length; i++) {
  359. for (fileName in coverObjects[i]) {
  360. gaps = coverObjects[i][fileName];
  361. if (!coverObject.hasOwnProperty(fileName)) {
  362. coverObject[fileName] = gaps;
  363. } else {
  364. for (gap in gaps) {
  365. if (gap === '__code') {
  366. continue;
  367. }
  368. count = gaps[gap];
  369. if (!coverObject[fileName].hasOwnProperty(gap)) {
  370. coverObject[fileName][gap] = count;
  371. } else {
  372. coverObject[fileName][gap] += count;
  373. }
  374. }
  375. }
  376. }
  377. }
  378. return coverObject;
  379. }
  380. function showCoverage() {
  381. var overlay, coverView, viewPortW, viewPortH;
  382. viewPortW = window.innerWidth || document.documentElement.clientWidth;
  383. viewPortH = window.innerHeight || document.documentElement.clientHeight;
  384. overlay = get('overlay');
  385. overlay.style.display = 'block';
  386. coverView = get('coverview');
  387. coverView.style.left = '30px';
  388. coverView.style.top = '30px';
  389. coverView.style.width = (viewPortW - 60) + 'px';
  390. coverView.style.height = (viewPortH - 60) + 'px';
  391. coverView.style.display = 'block';
  392. coverView.getElementsByTagName('iframe')[0].src = 'js/coverage/index.html';
  393. }
  394. function hideCoverage() {
  395. get('overlay').style.display = 'none';
  396. get('coverview').style.display = 'none';
  397. }
  398. return {
  399. init: init,
  400. addSuites: addSuites,
  401. reflow: reflow,
  402. done: done,
  403. addCoverObject: addCoverObject,
  404. getCoverObject: getCoverObject,
  405. showCoverage: showCoverage,
  406. hideCoverage: hideCoverage
  407. };
  408. }
  409. var testRunner = new TestRunner();
  410. self.onload = function() {
  411. testRunner.init();
  412. };
  413. self.onresize = function() {
  414. testRunner.reflow();
  415. };
  416. self.TestRunner = testRunner;
  417. })(window);