/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ var ui = ui || {}; ui.results = ui.results || {}; (function(){ var kResultsPrefetchDelayMS = 500; // FIXME: Rather than using table, should we be using something fancier? ui.results.Comparison = base.extends('table', { init: function() { this.className = 'comparison'; this.innerHTML = 'ExpectedActualDiff' + ''; }, _selectorForKind: function(kind) { switch (kind) { case results.kExpectedKind: return '.expected'; case results.kActualKind: return '.actual'; case results.kDiffKind: return '.diff'; } return '.unknown'; }, update: function(kind, result) { var selector = this._selectorForKind(kind); $(selector, this).empty().append(result); return result; }, }); // We'd really like TextResult and ImageResult to extend a common Result base // class, but we can't seem to do that because they inherit from different // HTMLElements. We could have them inherit from
, but that seems lame. ui.results.TextResult = base.extends('iframe', { init: function(url) { this.className = 'text-result'; this.src = url; } }); ui.results.ImageResult = base.extends('img', { init: function(url) { this.className = 'image-result'; this.src = url; } }); ui.results.AudioResult = base.extends('audio', { init: function(url) { this.className = 'audio-result'; this.src = url; this.controls = 'controls'; } }); function constructorForResultType(type) { if (type == results.kImageType) return ui.results.ImageResult; if (type == results.kAudioType) return ui.results.AudioResult; return ui.results.TextResult; } ui.results.ResultsGrid = base.extends('div', { init: function() { this.className = 'results-grid'; }, _addResult: function(comparison, constructor, resultsURLsByKind, kind) { var url = resultsURLsByKind[kind]; if (!url) return; comparison.update(kind, new constructor(url)); }, addComparison: function(resultType, resultsURLsByKind) { var comparison = new ui.results.Comparison(); var constructor = constructorForResultType(resultType); this._addResult(comparison, constructor, resultsURLsByKind, results.kExpectedKind); this._addResult(comparison, constructor, resultsURLsByKind, results.kActualKind); this._addResult(comparison, constructor, resultsURLsByKind, results.kDiffKind); this.appendChild(comparison); return comparison; }, addRow: function(resultType, url) { var constructor = constructorForResultType(resultType); var view = new constructor(url); this.appendChild(view); return view; }, addResults: function(resultsURLs) { var resultsURLsByTypeAndKind = {}; resultsURLsByTypeAndKind[results.kImageType] = {}; resultsURLsByTypeAndKind[results.kAudioType] = {}; resultsURLsByTypeAndKind[results.kTextType] = {}; resultsURLs.forEach(function(url) { resultsURLsByTypeAndKind[results.resultType(url)][results.resultKind(url)] = url; }); $.each(resultsURLsByTypeAndKind, function(resultType, resultsURLsByKind) { if ($.isEmptyObject(resultsURLsByKind)) return; if (results.kUnknownKind in resultsURLsByKind) { // This is something like "crash" that isn't a comparison. this.addRow(resultType, resultsURLsByKind[results.kUnknownKind]); return; } this.addComparison(resultType, resultsURLsByKind); }.bind(this)); if (!this.children.length) this.textContent = 'No results to display.' } }); ui.results.ResultsDetails = base.extends('div', { init: function(delegate, failureInfo) { this.className = 'results-detail'; this._delegate = delegate; this._failureInfo = failureInfo; this._haveShownOnce = false; }, show: function() { if (this._haveShownOnce) return; this._haveShownOnce = true; this._delegate.fetchResultsURLs(this._failureInfo, function(resultsURLs) { var resultsGrid = new ui.results.ResultsGrid(); resultsGrid.addResults(resultsURLs); $(this).empty().append( new ui.actions.List([ new ui.actions.Previous(), new ui.actions.Next() ])).append(resultsGrid); }.bind(this)); }, }); function isAnyReftest(testName, resultsByTest) { return Object.keys(resultsByTest[testName]).map(function(builder) { return resultsByTest[testName][builder]; }).some(function(resultNode) { return resultNode.reftest_type && resultNode.reftest_type.length; }); } ui.results.FlakinessData = base.extends('iframe', { init: function() { this.className = 'flakiness-iframe'; this.src = ui.urlForEmbeddedFlakinessDashboard(); this.addEventListener('load', function() { window.addEventListener('message', this._handleMessage.bind(this)); }); }, _handleMessage: function(event) { if (!this.contentWindow) return; if (event.data.command != 'heightChanged') { console.log('Unknown postMessage command: ' + event.data); return; } this.style.height = event.data.height + 'px'; } }); ui.results.TestSelector = base.extends('div', { init: function(delegate, resultsByTest) { this.className = 'test-selector'; this._delegate = delegate; var topPanel = document.createElement('div'); topPanel.className = 'top-panel'; this.appendChild(topPanel); this._appendResizeHandle(); var bottomPanel = document.createElement('div'); bottomPanel.className = 'bottom-panel'; this.appendChild(bottomPanel); this._flakinessData = new ui.results.FlakinessData(); this.appendChild(this._flakinessData); var testNames = Object.keys(resultsByTest); testNames.sort().forEach(function(testName) { var nonLinkTitle = document.createElement('a'); nonLinkTitle.classList.add('non-link-title'); nonLinkTitle.textContent = testName; var linkTitle = document.createElement('a'); linkTitle.classList.add('link-title'); linkTitle.setAttribute('href', ui.urlForFlakinessDashboard([testName])) linkTitle.target = '_blank'; linkTitle.textContent = testName; var header = document.createElement('h3'); header.appendChild(nonLinkTitle); header.appendChild(linkTitle); header.addEventListener('click', this._showResults.bind(this, header, false)); topPanel.appendChild(header); }, this); // If we have a small amount of content, don't show the resize handler. // Otherwise, set the minHeight so that the percentage height of the // topPanel is not too small. if (testNames.length <= 4) this.removeChild(this.querySelector('.resize-handle')); else topPanel.style.minHeight = '100px'; }, _appendResizeHandle: function() { var resizeHandle = document.createElement('div'); resizeHandle.className = 'resize-handle'; this.appendChild(resizeHandle); resizeHandle.addEventListener('mousedown', function(event) { this._is_resizing = true; event.preventDefault(); }.bind(this)); var cancelResize = function(event) { this._is_resizing = false; }.bind(this); this.addEventListener('mouseup', cancelResize); // FIXME: Use addEventListener once WebKit adds support for mouseleave/mouseenter. $(window).bind('mouseleave', cancelResize); this.addEventListener('mousemove', function(event) { if (!this._is_resizing) return; var mouseY = event.clientY + document.body.scrollTop - this.offsetTop; var percentage = 100 * mouseY / this.offsetHeight; document.querySelector('.top-panel').style.maxHeight = percentage + '%'; }.bind(this)) }, _showResults: function(header, scrollInfoView) { if (!header) return false; var activeHeader = this.querySelector('.active') if (activeHeader) activeHeader.classList.remove('active'); header.classList.add('active'); var testName = this.currentTestName(); this._flakinessData.src = ui.urlForEmbeddedFlakinessDashboard([testName]); var bottomPanel = this.querySelector('.bottom-panel') bottomPanel.innerHTML = ''; bottomPanel.appendChild(this._delegate.contentForTest(testName)); var topPanel = this.querySelector('.top-panel'); if (scrollInfoView) { topPanel.scrollTop = header.offsetTop; if (header.offsetTop - topPanel.scrollTop < header.offsetHeight) topPanel.scrollTop = topPanel.scrollTop - header.offsetHeight; } var resultsDetails = this.querySelectorAll('.results-detail'); if (resultsDetails.length) resultsDetails[0].show(); setTimeout(function() { Array.prototype.forEach.call(resultsDetails, function(resultsDetail) { resultsDetail.show(); }); }, kResultsPrefetchDelayMS); return true; }, nextResult: function() { if (this.querySelector('.builder-selector').nextResult()) return true; return this.nextTest(); }, previousResult: function() { if (this.querySelector('.builder-selector').previousResult()) return true; return this.previousTest(); }, nextTest: function() { return this._showResults(this.querySelector('.active').nextSibling, true); }, previousTest: function() { var succeeded = this._showResults(this.querySelector('.active').previousSibling, true); if (succeeded) this.querySelector('.builder-selector').lastResult(); return succeeded; }, firstResult: function() { this._showResults(this.querySelector('h3'), true); }, currentTestName: function() { return this.querySelector('.active .non-link-title').textContent; } }); ui.results.BuilderSelector = base.extends('div', { init: function(delegate, testName, resultsByBuilder) { this.className = 'builder-selector'; this._delegate = delegate; var tabStrip = this.appendChild(document.createElement('ul')); Object.keys(resultsByBuilder).sort().forEach(function(builderName) { var builderHash = base.underscoredBuilderName(builderName); var link = document.createElement('a'); $(link).attr('href', "#" + builderHash).text(ui.displayNameForBuilder(builderName)); tabStrip.appendChild(document.createElement('li')).appendChild(link); var content = this._delegate.contentForTestAndBuilder(testName, builderName); content.id = builderHash; this.appendChild(content); }, this); $(this).tabs(); }, nextResult: function() { var nextIndex = $(this).tabs('option', 'selected') + 1; if (nextIndex >= $(this).tabs('length')) return false $(this).tabs('option', 'selected', nextIndex); return true; }, previousResult: function() { var previousIndex = $(this).tabs('option', 'selected') - 1; if (previousIndex < 0) return false; $(this).tabs('option', 'selected', previousIndex); return true; }, firstResult: function() { $(this).tabs('option', 'selected', 0); }, lastResult: function() { $(this).tabs('option', 'selected', $(this).tabs('length') - 1); } }); ui.results.View = base.extends('div', { init: function(delegate) { this.className = 'results-view'; this._delegate = delegate; }, contentForTest: function(testName) { var rebaselineAction; if (isAnyReftest(testName, this._resultsByTest)) rebaselineAction = $('
Reftests cannot be rebaselined. Email webkit-gardening@chromium.org if unsure how to fix this.
'); else rebaselineAction = new ui.actions.List([new ui.actions.Rebaseline().makeDefault()]); $(rebaselineAction).addClass('rebaseline-action'); var builderSelector = new ui.results.BuilderSelector(this, testName, this._resultsByTest[testName]); $(builderSelector).append(rebaselineAction).append($('
')); $(builderSelector).bind('tabsselect', function(event, ui) { // We will probably have pre-fetched the tab already, but we need to make sure. ui.panel.show(); }); return builderSelector; }, contentForTestAndBuilder: function(testName, builderName) { var failureInfo = results.failureInfoForTestAndBuilder(this._resultsByTest, testName, builderName); return new ui.results.ResultsDetails(this, failureInfo); }, setResultsByTest: function(resultsByTest) { $(this).empty(); this._resultsByTest = resultsByTest; this._testSelector = new ui.results.TestSelector(this, resultsByTest); this.appendChild(this._testSelector); }, fetchResultsURLs: function(failureInfo, callback) { this._delegate.fetchResultsURLs(failureInfo, callback) }, nextResult: function() { return this._testSelector.nextResult(); }, previousResult: function() { return this._testSelector.previousResult(); }, nextTest: function() { return this._testSelector.nextTest(); }, previousTest: function() { return this._testSelector.previousTest(); }, firstResult: function() { this._testSelector.firstResult() }, currentTestName: function() { return this._testSelector.currentTestName() } }); })();