blob: 525542071bf3087de5090be936398fe73d62ca82 [file] [log] [blame]
/*
* 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 = '<thead><tr><th>Expected</th><th>Actual</th><th>Diff</th></tr></thead>' +
'<tbody><tr><td class="expected result-container"></td><td class="actual result-container"></td><td class="diff result-container"></td></tr></tbody>';
},
_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 <div>, 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;
// Check for null event.origin so that the unittests can get past this point.
// FIXME: Is this safe? In practice, there's no meaningful harm that can come from
// a malicious page sending us heightChanged commands, so it doesn't really matter.
if (event.origin !== 'null' && event.origin != 'https://webkit-test-results.webkit.org') {
console.log('Invalid origin: ' + event.origin);
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.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 = $('<div class="non-action-button">Reftests cannot be rebaselined. Email webkit-gardening@chromium.org if unsure how to fix this.</div>');
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($('<br style="clear:both">'));
$(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()
}
});
})();