4 <title>WebKit Test Results</title>
5 <link rel="stylesheet" href="/common.css">
6 <script src="js/autocompleter.js"></script>
7 <script src="js/build.js"></script>
8 <script src="js/dom.js"></script>
13 <h1><a href="/">WebKit Test Results</a></h1>
15 <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
19 <form id="navigationBar" onsubmit="TestResultsView.fetchTest(this['testName'].value);
20 TestResultsView.updateLocationHash();
22 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
23 placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
25 <div id="container"></div>
26 <div id="tooltipContainer"></div>
34 border: 1px solid #ccc;
40 border: 1px solid #ccc;
58 border-collapse: collapse;
59 border: 2px solid #fff;
64 .resultsTable caption {
74 border: 2px solid #fff;
80 .resultsTable .passingRate {
89 .resultsTable .passingRate {
93 .resultsTable .resultCell {
94 display: inline-block;
105 .resultsTable .PASS a {
106 background-color: #0c3;
109 .resultsTable .TEXT a {
110 background-color: #c33;
113 .resultsTable .IMAGE a {
114 background-color: #3cf;
117 .resultsTable .TEXT.PASS a {
118 background-color: #cf3;
121 .resultsTable .CRASH a {
122 background-color: #f00;
130 border: 1px solid #ccc;
138 .candidateWindow em {
139 background-color: #ccc;
143 .candidateWindow .selected {
144 background-color: #0cf;
174 border-color: #333 transparent transparent transparent;
187 text-decoration: underline;
193 var TestResultsView = new (function () {
194 var tooltipContainer = document.getElementById('tooltipContainer');
196 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
197 document.addEventListener('click', function (event) {
198 if (!event.insideTooltip)
199 tooltipContainer.style.display = 'none';
202 window.addEventListener('hashchange', function (event) {
203 TestResultsView.locationHashChanged();
206 this._tooltipContainer = tooltipContainer;
208 this._oldHash = null;
211 this._repositories = [];
214 TestResultsView.setBuilders = function (builders) {
215 this._builders = builders;
218 TestResultsView.setSlaves = function (slaves) {
219 this._slaves = slaves;
222 TestResultsView.setRepositories = function (repositories) {
223 this._repositories = repositories;
226 TestResultsView.showTooltip = function (anchor, contentElement) {
227 var tooltipContainer = this._tooltipContainer;
228 tooltipContainer.style.display = null;
230 while (tooltipContainer.firstChild)
231 tooltipContainer.removeChild(tooltipContainer.firstChild);
232 tooltipContainer.appendChild(contentElement);
234 var rect = anchor.getBoundingClientRect();
235 tooltipContainer.style.left = (rect.left - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
236 tooltipContainer.style.top = (rect.top - contentElement.clientHeight - 5) + 'px';
239 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
240 // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
242 "build": "http://$master/builders/$builder/builds/$build",
243 "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
244 }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
245 .replace(/\$revision/g, revision).replace(/\$build/g, build);
248 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
249 var buildTime = result['buildTime'];
250 var revision = result['revision'];
251 var slave = result['slave'];
252 var build = result['buildNumber'];
253 var actual = result['actual'];
254 var expected = result['expected'];
255 var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
256 anchor.onmouseenter = function () {
257 var repositoryById = TestResultsView._repositories;
258 var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
259 var revisionDescription = '';
260 for (var repositoryName in formattedRevisions) {
261 var revision = formattedRevisions[repositoryName];
262 if (revisionDescription)
263 revisionDescription += ', ';
265 revisionDescription += repositoryName + ': ' + revision.url;
267 revisionDescription += revision.label;
270 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
272 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
273 element('li', ['Revision: ' + revisionDescription]),
274 element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
275 element('li', ['Result: ' + actual]),
279 var cell = element('span', {
280 'class': actual + ' resultCell',
285 TestResultsView._populatePane = function(testName, results, section) {
286 var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
287 var resultsByBuilder = results['builders'];
288 for (var builderId in resultsByBuilder) {
289 var results = resultsByBuilder[builderId];
290 for (var i = 0; i < results.length; i++) {
291 results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
294 var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
295 var cells = new Array(sortedResults.length);
296 var builder = this._builders[builderId];
297 for (var i = 0; i < sortedResults.length; i++)
298 cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
300 var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
301 var passingRate = Math.round(passCount / cells.length * 100) + '%';
302 table.appendChild(element('tr', [element('th', [builder.name]), element('td', {'class': 'passingRate'}, [passingRate]),
303 element('td', cells)]));
304 // FIXME: Add a master name if there is more than one.
306 section.appendChild(table);
309 TestResultsView.fetchTest = function (testName) {
310 if (this._tests.indexOf(testName) >= 0)
315 var closeButton = element('div', {'class': 'closeButton'});
316 closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
317 + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
318 + '<polygon points="30,70 70,30"></polygon></g></svg>';
319 closeButton.addEventListener('click', function (event) {
320 self._removeTest(testName);
321 section.parentNode.removeChild(section);
322 event.preventDefault();
324 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton]);
326 document.getElementById('container').appendChild(section);
328 var xhr = new XMLHttpRequest();
329 xhr.open("GET", 'api/results.php?test=' + testName, true);
330 xhr.onload = function(event) {
331 var response = JSON.parse(xhr.response);
332 if (response['status'] != 'OK') {
333 section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
337 self._populatePane(testName, response, section);
340 this._tests.push(testName);
343 TestResultsView._removeTest = function (testName) {
344 var index = this._tests.indexOf(testName);
347 this._tests.splice(index, 1);
348 this.updateLocationHash();
351 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
352 for (var i = 0; i < testNames.length; i++)
353 this.fetchTest(testNames[i], doNotUpdateHash);
354 this.updateLocationHash();
357 TestResultsView.updateLocationHash = function () {
359 'tests': this._tests.join(',')
362 for (var key in params)
363 hash += decodeURIComponent(key) + '=' + decodeURIComponent(params[key]);
364 location.hash = hash;
365 this._oldHash = location.hash;
368 TestResultsView.locationHashChanged = function () {
369 var newHash = location.hash;
370 if (newHash == this._oldHash)
372 this._oldHash = newHash;
374 document.getElementById('container').innerHTML = '';
375 this.loadTestsFromLocationHash();
378 TestResultsView.loadTestsFromLocationHash = function () {
380 location.hash.substr(1).split('&').forEach(function (component) {
381 var equalPosition = component.indexOf('=');
382 if (equalPosition < 0)
384 var name = decodeURIComponent(component.substr(0, equalPosition));
385 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
387 if (!parsed['tests'])
389 var doNotUpdateHash = true;
390 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
393 function fetchManifest(callback) {
394 var xhr = new XMLHttpRequest();
395 xhr.open("GET", 'api/manifest.php', true);
396 xhr.onload = function(event) {
397 var response = JSON.parse(xhr.response);
398 if (response['status'] != 'OK') {
399 alert('Failed to load manifest:' + response['status']);
400 console.log(response);
408 fetchManifest(function (response) {
409 var testNames = response['tests'].map(function (test) { return test['name']; })
410 var input = document.getElementById('testName');
411 input.autocompleter = new Autocompleter(input, testNames);
413 TestResultsView.setBuilders(response['builders']);
414 TestResultsView.setSlaves(response['slaves']);
415 TestResultsView.setRepositories(response['repositories']);
416 TestResultsView.loadTestsFromLocationHash();
419 function pasteHelper(input, event) {
420 function removeJunkFromNRWTStdout(input) {
421 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
424 function removeJunkFromResultsPage(input) {
425 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
428 var text = event.clipboardData.getData('text/plain');
429 if (text.indexOf('\n') < 0)
432 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
433 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
434 event.preventDefault();