| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright (C) 2011 Google Inc. All rights reserved. | |
| 3 * | |
| 4 * Redistribution and use in source and binary forms, with or without | |
| 5 * modification, are permitted provided that the following conditions | |
| 6 * are met: | |
| 7 * 1. Redistributions of source code must retain the above copyright | |
| 8 * notice, this list of conditions and the following disclaimer. | |
| 9 * 2. Redistributions in binary form must reproduce the above copyright | |
| 10 * notice, this list of conditions and the following disclaimer in the | |
| 11 * documentation and/or other materials provided with the distribution. | |
| 12 * | |
| 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | |
| 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
| 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | |
| 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
| 23 * THE POSSIBILITY OF SUCH DAMAGE. | |
| 24 */ | |
| 25 | |
| 26 var ui = ui || {}; | |
| 27 ui.results = ui.results || {}; | |
| 28 | |
| 29 (function(){ | |
| 30 | |
| 31 var kResultsPrefetchDelayMS = 500; | |
| 32 | |
| 33 // FIXME: Rather than using table, should we be using something fancier? | |
| 34 ui.results.Comparison = base.extends('table', { | |
| 35 init: function() | |
| 36 { | |
| 37 this.className = 'comparison'; | |
| 38 this.innerHTML = '<thead><tr><th>Expected</th><th>Actual</th><th>Diff</t
h></tr></thead>' + | |
| 39 '<tbody><tr><td class="expected result-container"></td>
<td class="actual result-container"></td><td class="diff result-container"></td>
</tr></tbody>'; | |
| 40 }, | |
| 41 _selectorForKind: function(kind) | |
| 42 { | |
| 43 switch (kind) { | |
| 44 case results.kExpectedKind: | |
| 45 return '.expected'; | |
| 46 case results.kActualKind: | |
| 47 return '.actual'; | |
| 48 case results.kDiffKind: | |
| 49 return '.diff'; | |
| 50 } | |
| 51 return '.unknown'; | |
| 52 }, | |
| 53 update: function(kind, result) | |
| 54 { | |
| 55 var selector = this._selectorForKind(kind); | |
| 56 $(selector, this).empty().append(result); | |
| 57 return result; | |
| 58 }, | |
| 59 }); | |
| 60 | |
| 61 // We'd really like TextResult and ImageResult to extend a common Result base | |
| 62 // class, but we can't seem to do that because they inherit from different | |
| 63 // HTMLElements. We could have them inherit from <div>, but that seems lame. | |
| 64 | |
| 65 ui.results.TextResult = base.extends('iframe', { | |
| 66 init: function(url) | |
| 67 { | |
| 68 this.className = 'text-result'; | |
| 69 this.src = url; | |
| 70 } | |
| 71 }); | |
| 72 | |
| 73 ui.results.ImageResult = base.extends('img', { | |
| 74 init: function(url) | |
| 75 { | |
| 76 this.className = 'image-result'; | |
| 77 this.src = url; | |
| 78 } | |
| 79 }); | |
| 80 | |
| 81 ui.results.AudioResult = base.extends('audio', { | |
| 82 init: function(url) | |
| 83 { | |
| 84 this.className = 'audio-result'; | |
| 85 this.src = url; | |
| 86 this.controls = 'controls'; | |
| 87 } | |
| 88 }); | |
| 89 | |
| 90 function constructorForResultType(type) | |
| 91 { | |
| 92 if (type == results.kImageType) | |
| 93 return ui.results.ImageResult; | |
| 94 if (type == results.kAudioType) | |
| 95 return ui.results.AudioResult; | |
| 96 return ui.results.TextResult; | |
| 97 } | |
| 98 | |
| 99 ui.results.ResultsGrid = base.extends('div', { | |
| 100 init: function() | |
| 101 { | |
| 102 this.className = 'results-grid'; | |
| 103 }, | |
| 104 _addResult: function(comparison, constructor, resultsURLsByKind, kind) | |
| 105 { | |
| 106 var url = resultsURLsByKind[kind]; | |
| 107 if (!url) | |
| 108 return; | |
| 109 comparison.update(kind, new constructor(url)); | |
| 110 }, | |
| 111 addComparison: function(resultType, resultsURLsByKind) | |
| 112 { | |
| 113 var comparison = new ui.results.Comparison(); | |
| 114 var constructor = constructorForResultType(resultType); | |
| 115 | |
| 116 this._addResult(comparison, constructor, resultsURLsByKind, results.kExp
ectedKind); | |
| 117 this._addResult(comparison, constructor, resultsURLsByKind, results.kAct
ualKind); | |
| 118 this._addResult(comparison, constructor, resultsURLsByKind, results.kDif
fKind); | |
| 119 | |
| 120 this.appendChild(comparison); | |
| 121 return comparison; | |
| 122 }, | |
| 123 addRow: function(resultType, url) | |
| 124 { | |
| 125 var constructor = constructorForResultType(resultType); | |
| 126 var view = new constructor(url); | |
| 127 this.appendChild(view); | |
| 128 return view; | |
| 129 }, | |
| 130 addResults: function(resultsURLs) | |
| 131 { | |
| 132 var resultsURLsByTypeAndKind = {}; | |
| 133 | |
| 134 resultsURLsByTypeAndKind[results.kImageType] = {}; | |
| 135 resultsURLsByTypeAndKind[results.kAudioType] = {}; | |
| 136 resultsURLsByTypeAndKind[results.kTextType] = {}; | |
| 137 | |
| 138 resultsURLs.forEach(function(url) { | |
| 139 resultsURLsByTypeAndKind[results.resultType(url)][results.resultKind
(url)] = url; | |
| 140 }); | |
| 141 | |
| 142 $.each(resultsURLsByTypeAndKind, function(resultType, resultsURLsByKind)
{ | |
| 143 if ($.isEmptyObject(resultsURLsByKind)) | |
| 144 return; | |
| 145 if (results.kUnknownKind in resultsURLsByKind) { | |
| 146 // This is something like "crash" that isn't a comparison. | |
| 147 this.addRow(resultType, resultsURLsByKind[results.kUnknownKind])
; | |
| 148 return; | |
| 149 } | |
| 150 this.addComparison(resultType, resultsURLsByKind); | |
| 151 }.bind(this)); | |
| 152 | |
| 153 if (!this.children.length) | |
| 154 this.textContent = 'No results to display.' | |
| 155 } | |
| 156 }); | |
| 157 | |
| 158 ui.results.ResultsDetails = base.extends('div', { | |
| 159 init: function(delegate, failureInfo) | |
| 160 { | |
| 161 this.className = 'results-detail'; | |
| 162 this._delegate = delegate; | |
| 163 this._failureInfo = failureInfo; | |
| 164 this._haveShownOnce = false; | |
| 165 }, | |
| 166 show: function() { | |
| 167 if (this._haveShownOnce) | |
| 168 return; | |
| 169 this._haveShownOnce = true; | |
| 170 this._delegate.fetchResultsURLs(this._failureInfo, function(resultsURLs)
{ | |
| 171 var resultsGrid = new ui.results.ResultsGrid(); | |
| 172 resultsGrid.addResults(resultsURLs); | |
| 173 | |
| 174 $(this).empty().append( | |
| 175 new ui.actions.List([ | |
| 176 new ui.actions.Previous(), | |
| 177 new ui.actions.Next() | |
| 178 ])).append(resultsGrid); | |
| 179 | |
| 180 | |
| 181 }.bind(this)); | |
| 182 }, | |
| 183 }); | |
| 184 | |
| 185 function isAnyReftest(testName, resultsByTest) | |
| 186 { | |
| 187 return Object.keys(resultsByTest[testName]).map(function(builder) { | |
| 188 return resultsByTest[testName][builder]; | |
| 189 }).some(function(resultNode) { | |
| 190 return resultNode.reftest_type && resultNode.reftest_type.length; | |
| 191 }); | |
| 192 } | |
| 193 | |
| 194 ui.results.FlakinessData = base.extends('iframe', { | |
| 195 init: function() | |
| 196 { | |
| 197 this.className = 'flakiness-iframe'; | |
| 198 this.src = ui.urlForEmbeddedFlakinessDashboard(); | |
| 199 this.addEventListener('load', function() { | |
| 200 window.addEventListener('message', this._handleMessage.bind(this)); | |
| 201 }); | |
| 202 }, | |
| 203 _handleMessage: function(event) { | |
| 204 if (!this.contentWindow) | |
| 205 return; | |
| 206 | |
| 207 // Check for null event.origin so that the unittests can get past this p
oint. | |
| 208 // FIXME: Is this safe? In practice, there's no meaningful harm that can
come from | |
| 209 // a malicious page sending us heightChanged commands, so it doesn't rea
lly matter. | |
| 210 if (event.origin !== 'null' && event.origin != 'http://test-results.apps
pot.com') { | |
| 211 console.log('Invalid origin: ' + event.origin); | |
| 212 return; | |
| 213 } | |
| 214 | |
| 215 if (event.data.command != 'heightChanged') { | |
| 216 console.log('Unknown postMessage command: ' + event.data); | |
| 217 return; | |
| 218 } | |
| 219 | |
| 220 this.style.height = event.data.height + 'px'; | |
| 221 } | |
| 222 }); | |
| 223 | |
| 224 ui.results.TestSelector = base.extends('div', { | |
| 225 init: function(delegate, resultsByTest) | |
| 226 { | |
| 227 this.className = 'test-selector'; | |
| 228 this._delegate = delegate; | |
| 229 | |
| 230 var topPanel = document.createElement('div'); | |
| 231 topPanel.className = 'top-panel'; | |
| 232 this.appendChild(topPanel); | |
| 233 | |
| 234 this._appendResizeHandle(); | |
| 235 | |
| 236 var bottomPanel = document.createElement('div'); | |
| 237 bottomPanel.className = 'bottom-panel'; | |
| 238 this.appendChild(bottomPanel); | |
| 239 | |
| 240 this._flakinessData = new ui.results.FlakinessData(); | |
| 241 this.appendChild(this._flakinessData); | |
| 242 | |
| 243 var testNames = Object.keys(resultsByTest); | |
| 244 testNames.sort().forEach(function(testName) { | |
| 245 var nonLinkTitle = document.createElement('a'); | |
| 246 nonLinkTitle.classList.add('non-link-title'); | |
| 247 nonLinkTitle.textContent = testName; | |
| 248 | |
| 249 var linkTitle = document.createElement('a'); | |
| 250 linkTitle.classList.add('link-title'); | |
| 251 linkTitle.setAttribute('href', ui.urlForFlakinessDashboard([testName
])) | |
| 252 linkTitle.textContent = testName; | |
| 253 | |
| 254 var header = document.createElement('h3'); | |
| 255 header.appendChild(nonLinkTitle); | |
| 256 header.appendChild(linkTitle); | |
| 257 header.addEventListener('click', this._showResults.bind(this, header
, false)); | |
| 258 topPanel.appendChild(header); | |
| 259 }, this); | |
| 260 | |
| 261 // If we have a small amount of content, don't show the resize handler. | |
| 262 // Otherwise, set the minHeight so that the percentage height of the | |
| 263 // topPanel is not too small. | |
| 264 if (testNames.length <= 4) | |
| 265 this.removeChild(this.querySelector('.resize-handle')); | |
| 266 else | |
| 267 topPanel.style.minHeight = '100px'; | |
| 268 }, | |
| 269 _appendResizeHandle: function() | |
| 270 { | |
| 271 var resizeHandle = document.createElement('div'); | |
| 272 resizeHandle.className = 'resize-handle'; | |
| 273 this.appendChild(resizeHandle); | |
| 274 | |
| 275 resizeHandle.addEventListener('mousedown', function(event) { | |
| 276 this._is_resizing = true; | |
| 277 event.preventDefault(); | |
| 278 }.bind(this)); | |
| 279 | |
| 280 var cancelResize = function(event) { this._is_resizing = false; }.bind(t
his); | |
| 281 this.addEventListener('mouseup', cancelResize); | |
| 282 // FIXME: Use addEventListener once WebKit adds support for mouseleave/m
ouseenter. | |
| 283 $(window).bind('mouseleave', cancelResize); | |
| 284 | |
| 285 this.addEventListener('mousemove', function(event) { | |
| 286 if (!this._is_resizing) | |
| 287 return; | |
| 288 var mouseY = event.clientY + document.body.scrollTop - this.offsetTo
p; | |
| 289 var percentage = 100 * mouseY / this.offsetHeight; | |
| 290 document.querySelector('.top-panel').style.maxHeight = percentage +
'%'; | |
| 291 }.bind(this)) | |
| 292 }, | |
| 293 _showResults: function(header, scrollInfoView) | |
| 294 { | |
| 295 if (!header) | |
| 296 return false; | |
| 297 | |
| 298 var activeHeader = this.querySelector('.active') | |
| 299 if (activeHeader) | |
| 300 activeHeader.classList.remove('active'); | |
| 301 header.classList.add('active'); | |
| 302 | |
| 303 var testName = this.currentTestName(); | |
| 304 this._flakinessData.src = ui.urlForEmbeddedFlakinessDashboard([testName]
); | |
| 305 | |
| 306 var bottomPanel = this.querySelector('.bottom-panel') | |
| 307 bottomPanel.innerHTML = ''; | |
| 308 bottomPanel.appendChild(this._delegate.contentForTest(testName)); | |
| 309 | |
| 310 var topPanel = this.querySelector('.top-panel'); | |
| 311 if (scrollInfoView) { | |
| 312 topPanel.scrollTop = header.offsetTop; | |
| 313 if (header.offsetTop - topPanel.scrollTop < header.offsetHeight) | |
| 314 topPanel.scrollTop = topPanel.scrollTop - header.offsetHeight; | |
| 315 } | |
| 316 | |
| 317 var resultsDetails = this.querySelectorAll('.results-detail'); | |
| 318 if (resultsDetails.length) | |
| 319 resultsDetails[0].show(); | |
| 320 setTimeout(function() { | |
| 321 Array.prototype.forEach.call(resultsDetails, function(resultsDetail)
{ | |
| 322 resultsDetail.show(); | |
| 323 }); | |
| 324 }, kResultsPrefetchDelayMS); | |
| 325 | |
| 326 return true; | |
| 327 }, | |
| 328 nextResult: function() | |
| 329 { | |
| 330 if (this.querySelector('.builder-selector').nextResult()) | |
| 331 return true; | |
| 332 return this.nextTest(); | |
| 333 }, | |
| 334 previousResult: function() | |
| 335 { | |
| 336 if (this.querySelector('.builder-selector').previousResult()) | |
| 337 return true; | |
| 338 return this.previousTest(); | |
| 339 }, | |
| 340 nextTest: function() | |
| 341 { | |
| 342 return this._showResults(this.querySelector('.active').nextSibling, true
); | |
| 343 }, | |
| 344 previousTest: function() | |
| 345 { | |
| 346 var succeeded = this._showResults(this.querySelector('.active').previous
Sibling, true); | |
| 347 if (succeeded) | |
| 348 this.querySelector('.builder-selector').lastResult(); | |
| 349 return succeeded; | |
| 350 }, | |
| 351 firstResult: function() | |
| 352 { | |
| 353 this._showResults(this.querySelector('h3'), true); | |
| 354 }, | |
| 355 currentTestName: function() | |
| 356 { | |
| 357 return this.querySelector('.active .non-link-title').textContent; | |
| 358 } | |
| 359 }); | |
| 360 | |
| 361 ui.results.BuilderSelector = base.extends('div', { | |
| 362 init: function(delegate, testName, resultsByBuilder) | |
| 363 { | |
| 364 this.className = 'builder-selector'; | |
| 365 this._delegate = delegate; | |
| 366 | |
| 367 var tabStrip = this.appendChild(document.createElement('ul')); | |
| 368 | |
| 369 Object.keys(resultsByBuilder).sort().forEach(function(builderName) { | |
| 370 var builderHash = base.underscoredBuilderName(builderName); | |
| 371 | |
| 372 var link = document.createElement('a'); | |
| 373 $(link).attr('href', "#" + builderHash).text(ui.displayNameForBuilde
r(builderName)); | |
| 374 tabStrip.appendChild(document.createElement('li')).appendChild(link)
; | |
| 375 | |
| 376 var content = this._delegate.contentForTestAndBuilder(testName, buil
derName); | |
| 377 content.id = builderHash; | |
| 378 this.appendChild(content); | |
| 379 }, this); | |
| 380 | |
| 381 $(this).tabs(); | |
| 382 }, | |
| 383 nextResult: function() | |
| 384 { | |
| 385 var nextIndex = $(this).tabs('option', 'selected') + 1; | |
| 386 if (nextIndex >= $(this).tabs('length')) | |
| 387 return false | |
| 388 $(this).tabs('option', 'selected', nextIndex); | |
| 389 return true; | |
| 390 }, | |
| 391 previousResult: function() | |
| 392 { | |
| 393 var previousIndex = $(this).tabs('option', 'selected') - 1; | |
| 394 if (previousIndex < 0) | |
| 395 return false; | |
| 396 $(this).tabs('option', 'selected', previousIndex); | |
| 397 return true; | |
| 398 }, | |
| 399 firstResult: function() | |
| 400 { | |
| 401 $(this).tabs('option', 'selected', 0); | |
| 402 }, | |
| 403 lastResult: function() | |
| 404 { | |
| 405 $(this).tabs('option', 'selected', $(this).tabs('length') - 1); | |
| 406 } | |
| 407 }); | |
| 408 | |
| 409 ui.results.View = base.extends('div', { | |
| 410 init: function(delegate) | |
| 411 { | |
| 412 this.className = 'results-view'; | |
| 413 this._delegate = delegate; | |
| 414 }, | |
| 415 contentForTest: function(testName) | |
| 416 { | |
| 417 var rebaselineAction; | |
| 418 if (isAnyReftest(testName, this._resultsByTest)) | |
| 419 rebaselineAction = $('<div class="non-action-button">Reftests cannot
be rebaselined. Email webkit-gardening@chromium.org if unsure how to fix this.<
/div>'); | |
| 420 else | |
| 421 rebaselineAction = new ui.actions.List([new ui.actions.Rebaseline().
makeDefault()]); | |
| 422 $(rebaselineAction).addClass('rebaseline-action'); | |
| 423 | |
| 424 var builderSelector = new ui.results.BuilderSelector(this, testName, thi
s._resultsByTest[testName]); | |
| 425 $(builderSelector).append(rebaselineAction).append($('<br style="clear:b
oth">')); | |
| 426 $(builderSelector).bind('tabsselect', function(event, ui) { | |
| 427 // We will probably have pre-fetched the tab already, but we need to
make sure. | |
| 428 ui.panel.show(); | |
| 429 }); | |
| 430 return builderSelector; | |
| 431 }, | |
| 432 contentForTestAndBuilder: function(testName, builderName) | |
| 433 { | |
| 434 var failureInfo = results.failureInfoForTestAndBuilder(this._resultsByTe
st, testName, builderName); | |
| 435 return new ui.results.ResultsDetails(this, failureInfo); | |
| 436 }, | |
| 437 setResultsByTest: function(resultsByTest) | |
| 438 { | |
| 439 $(this).empty(); | |
| 440 this._resultsByTest = resultsByTest; | |
| 441 this._testSelector = new ui.results.TestSelector(this, resultsByTest); | |
| 442 this.appendChild(this._testSelector); | |
| 443 }, | |
| 444 fetchResultsURLs: function(failureInfo, callback) | |
| 445 { | |
| 446 this._delegate.fetchResultsURLs(failureInfo, callback) | |
| 447 }, | |
| 448 nextResult: function() | |
| 449 { | |
| 450 return this._testSelector.nextResult(); | |
| 451 }, | |
| 452 previousResult: function() | |
| 453 { | |
| 454 return this._testSelector.previousResult(); | |
| 455 }, | |
| 456 nextTest: function() | |
| 457 { | |
| 458 return this._testSelector.nextTest(); | |
| 459 }, | |
| 460 previousTest: function() | |
| 461 { | |
| 462 return this._testSelector.previousTest(); | |
| 463 }, | |
| 464 firstResult: function() | |
| 465 { | |
| 466 this._testSelector.firstResult() | |
| 467 }, | |
| 468 currentTestName: function() | |
| 469 { | |
| 470 return this._testSelector.currentTestName() | |
| 471 } | |
| 472 }); | |
| 473 | |
| 474 })(); | |
| OLD | NEW |