2064 lines
60 KiB
HTML
2064 lines
60 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font-family: Helvetica, sans-serif;
|
|
font-size: 11pt;
|
|
}
|
|
|
|
body > * {
|
|
margin-left: 4px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 14pt;
|
|
margin-top: 1.5em;
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 0.3em;
|
|
}
|
|
|
|
a.clickable {
|
|
color: blue;
|
|
cursor: pointer;
|
|
margin-left: 0.2em;
|
|
}
|
|
|
|
tr:not(.results-row) td {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
tr:not(.results-row) td:first-of-type {
|
|
white-space: normal;
|
|
}
|
|
|
|
td:not(:first-of-type) {
|
|
text-transform: lowercase;
|
|
}
|
|
|
|
th, td {
|
|
padding: 1px 4px;
|
|
vertical-align: top;
|
|
}
|
|
|
|
td:nth-child(1) {
|
|
min-width: 35em;
|
|
}
|
|
|
|
th:empty, td:empty {
|
|
padding: 0;
|
|
}
|
|
|
|
th {
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
}
|
|
|
|
dt > sup {
|
|
vertical-align:text-top;
|
|
font-size:75%;
|
|
}
|
|
|
|
sup > a {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.content-container {
|
|
min-height: 0;
|
|
}
|
|
|
|
.note {
|
|
color: gray;
|
|
font-size: smaller;
|
|
}
|
|
|
|
.results-row {
|
|
background-color: white;
|
|
}
|
|
|
|
.results-row iframe, .results-row img {
|
|
width: 800px;
|
|
height: 600px;
|
|
}
|
|
|
|
.results-row[data-expanded="false"] {
|
|
display: none;
|
|
}
|
|
|
|
#toolbar {
|
|
position: fixed;
|
|
top: 2px;
|
|
right: 2px;
|
|
text-align: right;
|
|
}
|
|
|
|
.floating-panel {
|
|
padding: 6px;
|
|
background-color: rgba(255, 255, 255, 0.9);
|
|
border: 1px solid silver;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.expand-button {
|
|
background-color: white;
|
|
width: 11px;
|
|
height: 12px;
|
|
border: 1px solid gray;
|
|
display: inline-block;
|
|
margin: 0 3px 0 0;
|
|
position: relative;
|
|
cursor: default;
|
|
}
|
|
|
|
.current {
|
|
color: red;
|
|
}
|
|
|
|
.current .expand-button {
|
|
border-color: red;
|
|
}
|
|
|
|
.expand-button-text {
|
|
position: absolute;
|
|
top: -0.3em;
|
|
left: 1px;
|
|
}
|
|
|
|
tbody .flag {
|
|
display: none;
|
|
}
|
|
|
|
tbody.flagged .flag {
|
|
display: inline;
|
|
}
|
|
|
|
.stopped-running-early-message {
|
|
border: 3px solid #d00;
|
|
font-weight: bold;
|
|
display: inline-block;
|
|
padding: 3px;
|
|
}
|
|
|
|
.result-container {
|
|
display: inline-block;
|
|
border: 1px solid gray;
|
|
margin: 4px;
|
|
}
|
|
|
|
.result-container iframe, .result-container img {
|
|
border: 0;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.leaks > table {
|
|
margin: 4px;
|
|
}
|
|
.leaks > td {
|
|
padding-left: 20px;
|
|
}
|
|
|
|
#options {
|
|
background-color: white;
|
|
}
|
|
|
|
#options-menu {
|
|
border: 1px solid gray;
|
|
border-radius: 4px;
|
|
margin-top: 1px;
|
|
padding: 2px 4px;
|
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
|
|
transition: opacity .2s;
|
|
text-align: left;
|
|
position: absolute;
|
|
right: 4px;
|
|
background-color: white;
|
|
}
|
|
|
|
#options-menu label {
|
|
display: block;
|
|
}
|
|
|
|
.hidden-menu {
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
}
|
|
|
|
.label {
|
|
padding-left: 3px;
|
|
font-weight: bold;
|
|
font-size: small;
|
|
background-color: silver;
|
|
}
|
|
|
|
.pixel-zoom-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 96%;
|
|
margin: 10px;
|
|
padding: 10px;
|
|
display: -webkit-box;
|
|
display: -moz-box;
|
|
pointer-events: none;
|
|
background-color: silver;
|
|
border-radius: 20px;
|
|
border: 1px solid gray;
|
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.75);
|
|
}
|
|
|
|
.pixel-zoom-container > * {
|
|
-webkit-box-flex: 1;
|
|
-moz-box-flex: 1;
|
|
border: 1px solid black;
|
|
margin: 4px;
|
|
overflow: hidden;
|
|
background-color: white;
|
|
}
|
|
|
|
.pixel-zoom-container .scaled-image-container {
|
|
position: relative;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
|
|
.scaled-image-container > img {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
image-rendering: -webkit-optimize-contrast;
|
|
}
|
|
|
|
#flagged-test-container {
|
|
position: fixed;
|
|
bottom: 4px;
|
|
right: 4px;
|
|
width: 50%;
|
|
min-width: 400px;
|
|
background-color: rgba(255, 255, 255, 0.75);
|
|
}
|
|
|
|
#flagged-test-container > h2 {
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
#flagged-tests {
|
|
padding: 0 5px;
|
|
margin: 0;
|
|
height: 7em;
|
|
overflow-y: scroll;
|
|
}
|
|
</style>
|
|
<style id="unexpected-style"></style>
|
|
|
|
<script>
|
|
|
|
class Utils
|
|
{
|
|
static matchesSelector(node, selector)
|
|
{
|
|
if (node.matches)
|
|
return node.matches(selector);
|
|
|
|
if (node.webkitMatchesSelector)
|
|
return node.webkitMatchesSelector(selector);
|
|
|
|
if (node.mozMatchesSelector)
|
|
return node.mozMatchesSelector(selector);
|
|
}
|
|
|
|
static parentOfType(node, selector)
|
|
{
|
|
while (node = node.parentNode) {
|
|
if (Utils.matchesSelector(node, selector))
|
|
return node;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static stripExtension(testName)
|
|
{
|
|
// Temporary fix, also in Tools/Scripts/webkitpy/layout_tests/constrollers/test_result_writer.py, line 95.
|
|
// FIXME: Refactor to avoid confusing reference to both test and process names.
|
|
if (Utils.splitExtension(testName)[1].length > 5)
|
|
return testName;
|
|
return Utils.splitExtension(testName)[0];
|
|
}
|
|
|
|
static splitExtension(testName)
|
|
{
|
|
let index = testName.lastIndexOf('.');
|
|
if (index == -1) {
|
|
return [testName, ''];
|
|
}
|
|
return [testName.substring(0, index), testName.substring(index + 1)];
|
|
}
|
|
|
|
static forEach(nodeList, handler)
|
|
{
|
|
Array.prototype.forEach.call(nodeList, handler);
|
|
}
|
|
|
|
static toArray(nodeList)
|
|
{
|
|
return Array.prototype.slice.call(nodeList);
|
|
}
|
|
|
|
static trim(string)
|
|
{
|
|
return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
|
|
}
|
|
|
|
static async(func, args)
|
|
{
|
|
setTimeout(() => { func.apply(null, args); }, 50);
|
|
}
|
|
|
|
static appendHTML(node, html)
|
|
{
|
|
if (node.insertAdjacentHTML)
|
|
node.insertAdjacentHTML('beforeEnd', html);
|
|
else
|
|
node.innerHTML += html;
|
|
}};
|
|
|
|
class TestResult
|
|
{
|
|
constructor(info, name)
|
|
{
|
|
this.name = name;
|
|
this.info = info; // FIXME: make this private.
|
|
}
|
|
|
|
isFailureExpected()
|
|
{
|
|
let actual = this.info.actual;
|
|
let expected = this.info.expected || 'PASS';
|
|
|
|
if (actual != 'SKIP') {
|
|
let expectedArray = expected.split(' ');
|
|
let actualArray = actual.split(' ');
|
|
for (let actualValue of actualArray) {
|
|
if (expectedArray.indexOf(actualValue) == -1 && (expectedArray.indexOf('FAIL') == -1 || (actualValue != 'TEXT' && actualValue != 'IMAGE+TEXT' && actualValue != 'AUDIO')))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
isMissingAllResults()
|
|
{
|
|
return this.info.actual == 'MISSING';
|
|
}
|
|
|
|
hasMissingResult()
|
|
{
|
|
return this.info.actual.indexOf('MISSING') != -1;
|
|
}
|
|
|
|
isFlakey(pixelTestsEnabled)
|
|
{
|
|
let actualTokens = this.info.actual.split(' ');
|
|
let passedWithImageOnlyFailureInRetry = actualTokens[0] == 'TEXT' && actualTokens[1] == 'IMAGE';
|
|
if (actualTokens[1] && this.info.actual.indexOf('PASS') != -1 || (!pixelTestsEnabled && passedWithImageOnlyFailureInRetry))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
isPass()
|
|
{
|
|
return this.info.actual == 'PASS';
|
|
}
|
|
|
|
isTextFailure()
|
|
{
|
|
return this.info.actual.indexOf('TEXT') != -1;
|
|
}
|
|
|
|
isImageFailure()
|
|
{
|
|
return this.info.actual.indexOf('IMAGE') != -1;
|
|
}
|
|
|
|
isAudioFailure()
|
|
{
|
|
return this.info.actual.indexOf('AUDIO') != -1;
|
|
}
|
|
|
|
hasLeak()
|
|
{
|
|
return this.info.actual.indexOf('LEAK') != -1;
|
|
}
|
|
|
|
isCrash()
|
|
{
|
|
return this.info.actual == 'CRASH';
|
|
}
|
|
|
|
isTimeout()
|
|
{
|
|
return this.info.actual == 'TIMEOUT';
|
|
}
|
|
|
|
isUnexpectedPass(pixelTestsEnabled)
|
|
{
|
|
if (this.info.actual == 'PASS' && this.info.expected != 'PASS') {
|
|
if (this.info.expected != 'IMAGE' || (pixelTestsEnabled || this.isRefTest()))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
isRefTest()
|
|
{
|
|
return !!this.info.reftest_type;
|
|
}
|
|
|
|
isMismatchRefTest()
|
|
{
|
|
return this.isRefTest() && this.info.reftest_type.indexOf('!=') != -1;
|
|
}
|
|
|
|
isMatchRefTest()
|
|
{
|
|
return this.isRefTest() && this.info.reftest_type.indexOf('==') != -1;
|
|
}
|
|
|
|
isMissingText()
|
|
{
|
|
return this.info.is_missing_text;
|
|
}
|
|
|
|
isMissingImage()
|
|
{
|
|
return this.info.is_missing_image;
|
|
}
|
|
|
|
isMissingAudio()
|
|
{
|
|
return this.info.is_missing_audio;
|
|
}
|
|
|
|
hasStdErr()
|
|
{
|
|
return this.info.has_stderr;
|
|
}
|
|
};
|
|
|
|
class TestResults
|
|
{
|
|
constructor(results)
|
|
{
|
|
this._results = results;
|
|
|
|
this.crashTests = [];
|
|
this.crashOther = [];
|
|
this.missingResults = [];
|
|
this.failingTests = [];
|
|
this.testsWithStderr = [];
|
|
this.timeoutTests = [];
|
|
this.unexpectedPassTests = [];
|
|
this.flakyPassTests = [];
|
|
|
|
this.hasHttpTests = false;
|
|
this.hasImageFailures = false;
|
|
this.hasTextFailures = false;
|
|
|
|
this._testsByName = new Map;
|
|
|
|
this._forEachTest(this._results.tests, '');
|
|
this._forOtherCrashes(this._results.other_crashes);
|
|
}
|
|
|
|
date()
|
|
{
|
|
return this._results.date;
|
|
}
|
|
|
|
layoutTestsDir()
|
|
{
|
|
return this._results.layout_tests_dir;
|
|
}
|
|
|
|
usesExpectationsFile()
|
|
{
|
|
return this._results.uses_expectations_file;
|
|
}
|
|
|
|
resultForTest(testName)
|
|
{
|
|
return this._resultsByTest[testName];
|
|
}
|
|
|
|
wasInterrupted()
|
|
{
|
|
return this._results.interrupted;
|
|
}
|
|
|
|
hasPrettyPatch()
|
|
{
|
|
return this._results.has_pretty_patch;
|
|
}
|
|
|
|
hasWDiff()
|
|
{
|
|
return this._results.has_wdiff;
|
|
}
|
|
|
|
testWithName(testName)
|
|
{
|
|
return this._testsByName.get(testName);
|
|
}
|
|
|
|
_processResultForTest(testResult)
|
|
{
|
|
this._testsByName.set(testResult.name, testResult);
|
|
|
|
let test = testResult.name;
|
|
if (testResult.hasStdErr())
|
|
this.testsWithStderr.push(testResult);
|
|
|
|
this.hasHttpTests |= test.indexOf('http/') == 0;
|
|
|
|
if (this.usesExpectationsFile())
|
|
testResult.isExpected = testResult.isFailureExpected();
|
|
|
|
if (testResult.isTextFailure())
|
|
this.hasTextFailures = true;
|
|
|
|
if (testResult.isImageFailure())
|
|
this.hasImageFailures = true;
|
|
|
|
if (testResult.isMissingAllResults()) {
|
|
// FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for tests with MISSING results.
|
|
this.missingResults.push(testResult);
|
|
return;
|
|
}
|
|
|
|
if (testResult.isFlakey(this._results.pixel_tests_enabled)) {
|
|
this.flakyPassTests.push(testResult);
|
|
return;
|
|
}
|
|
|
|
if (testResult.isPass()) {
|
|
if (testResult.isUnexpectedPass(this._results.pixel_tests_enabled))
|
|
this.unexpectedPassTests.push(testResult);
|
|
return;
|
|
}
|
|
|
|
if (testResult.isCrash()) {
|
|
this.crashTests.push(testResult);
|
|
return;
|
|
}
|
|
|
|
if (testResult.isTimeout()) {
|
|
this.timeoutTests.push(testResult);
|
|
return;
|
|
}
|
|
|
|
this.failingTests.push(testResult);
|
|
}
|
|
|
|
_forEachTest(tree, prefix)
|
|
{
|
|
for (let key in tree) {
|
|
let newPrefix = prefix ? (prefix + '/' + key) : key;
|
|
if ('actual' in tree[key]) {
|
|
let testObject = new TestResult(tree[key], newPrefix);
|
|
this._processResultForTest(testObject);
|
|
} else
|
|
this._forEachTest(tree[key], newPrefix);
|
|
}
|
|
}
|
|
|
|
_forOtherCrashes(tree)
|
|
{
|
|
for (let key in tree) {
|
|
let testObject = new TestResult(tree[key], key);
|
|
this.crashOther.push(testObject);
|
|
}
|
|
}
|
|
|
|
static sortByName(tests)
|
|
{
|
|
tests.sort(function (a, b) { return a.name.localeCompare(b.name) });
|
|
}
|
|
|
|
static hasUnexpectedResult(tests)
|
|
{
|
|
return tests.some(function (test) { return !test.isExpected; });
|
|
}
|
|
};
|
|
|
|
class TestResultsController
|
|
{
|
|
constructor(containerElement, testResults)
|
|
{
|
|
this.containerElement = containerElement;
|
|
this.testResults = testResults;
|
|
|
|
this.shouldToggleImages = true;
|
|
this._togglingImageInterval = null;
|
|
|
|
this._updatePageTitle();
|
|
|
|
this.buildResultsTables();
|
|
this.hideNonApplicableUI();
|
|
this.setupSorting();
|
|
this.setupOptions();
|
|
}
|
|
|
|
buildResultsTables()
|
|
{
|
|
if (this.testResults.wasInterrupted()) {
|
|
let interruptionMessage = document.createElement('p');
|
|
interruptionMessage.textContent = 'Testing exited early';
|
|
interruptionMessage.classList.add('stopped-running-early-message');
|
|
this.containerElement.appendChild(interruptionMessage);
|
|
}
|
|
|
|
if (this.testResults.crashTests.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.crashTests, 'crash-tests-table'));
|
|
|
|
if (this.testResults.crashOther.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.crashOther, 'other-crash-tests-table'));
|
|
|
|
if (this.testResults.failingTests.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.failingTests, 'results-table'));
|
|
|
|
if (this.testResults.missingResults.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.missingResults, 'missing-table'));
|
|
|
|
if (this.testResults.timeoutTests.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.timeoutTests, 'timeout-tests-table'));
|
|
|
|
if (this.testResults.testsWithStderr.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.testsWithStderr, 'stderr-table'));
|
|
|
|
if (this.testResults.flakyPassTests.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.flakyPassTests, 'flaky-tests-table'));
|
|
|
|
if (this.testResults.usesExpectationsFile() && this.testResults.unexpectedPassTests.length)
|
|
this.containerElement.appendChild(this.buildOneSection(this.testResults.unexpectedPassTests, 'passes-table'));
|
|
|
|
if (this.testResults.hasHttpTests) {
|
|
let httpdAccessLogLink = document.createElement('p');
|
|
httpdAccessLogLink.innerHTML = 'httpd access log: <a href="access_log.txt">access_log.txt</a>';
|
|
|
|
let httpdErrorLogLink = document.createElement('p');
|
|
httpdErrorLogLink.innerHTML = 'httpd error log: <a href="error_log.txt">error_log.txt</a>';
|
|
|
|
this.containerElement.appendChild(httpdAccessLogLink);
|
|
this.containerElement.appendChild(httpdErrorLogLink);
|
|
}
|
|
|
|
this.updateTestlistCounts();
|
|
}
|
|
|
|
static sectionBuilderClassForTableID(tableID)
|
|
{
|
|
const idToBuilderClassMap = {
|
|
'crash-tests-table' : CrashingTestsSectionBuilder,
|
|
'other-crash-tests-table' : OtherCrashesSectionBuilder,
|
|
'results-table' : FailingTestsSectionBuilder,
|
|
'missing-table' : TestsWithMissingResultsSectionBuilder,
|
|
'timeout-tests-table' : TimedOutTestsSectionBuilder,
|
|
'stderr-table' : TestsWithStdErrSectionBuilder,
|
|
'flaky-tests-table' : FlakyPassTestsSectionBuilder,
|
|
'passes-table' : UnexpectedPassTestsSectionBuilder,
|
|
};
|
|
return idToBuilderClassMap[tableID];
|
|
}
|
|
|
|
setupSorting()
|
|
{
|
|
let resultsTable = document.getElementById('results-table');
|
|
if (!resultsTable)
|
|
return;
|
|
|
|
// FIXME: Make all the tables sortable. Maybe SectionBuilder should put a TableSorter on each table.
|
|
resultsTable.addEventListener('click', TableSorter.handleClick, false);
|
|
TableSorter.sortColumn(0);
|
|
}
|
|
|
|
hideNonApplicableUI()
|
|
{
|
|
// FIXME: do this all through body classnames.
|
|
if (!this.testResults.hasTextFailures) {
|
|
let textResultsHeader = document.getElementById('text-results-header');
|
|
if (textResultsHeader)
|
|
textResultsHeader.textContent = '';
|
|
}
|
|
|
|
if (!this.testResults.hasImageFailures) {
|
|
let imageResultsHeader = document.getElementById('image-results-header');
|
|
if (imageResultsHeader)
|
|
imageResultsHeader.textContent = '';
|
|
|
|
Utils.parentOfType(document.getElementById('toggle-images'), 'label').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
setupOptions()
|
|
{
|
|
// FIXME: do this all through body classnames.
|
|
if (!this.testResults.usesExpectationsFile())
|
|
Utils.parentOfType(document.getElementById('unexpected-results'), 'label').style.display = 'none';
|
|
}
|
|
|
|
buildOneSection(tests, tableID)
|
|
{
|
|
TestResults.sortByName(tests);
|
|
|
|
let sectionBuilderClass = TestResultsController.sectionBuilderClassForTableID(tableID);
|
|
let sectionBuilder = new sectionBuilderClass(tests, tableID, this);
|
|
return sectionBuilder.build();
|
|
}
|
|
|
|
updateTestlistCounts()
|
|
{
|
|
// FIXME: do this through the data model, not through the DOM.
|
|
let onlyShowUnexpectedFailures = this.onlyShowUnexpectedFailures();
|
|
Utils.forEach(document.querySelectorAll('.test-list-count'), count => {
|
|
let container = Utils.parentOfType(count, 'section');
|
|
let testContainers;
|
|
if (onlyShowUnexpectedFailures)
|
|
testContainers = container.querySelectorAll('tbody:not(.expected)');
|
|
else
|
|
testContainers = container.querySelectorAll('tbody');
|
|
|
|
count.textContent = testContainers.length;
|
|
})
|
|
}
|
|
|
|
flagAll(headerLink)
|
|
{
|
|
let tests = this.visibleTests(Utils.parentOfType(headerLink, 'section'));
|
|
Utils.forEach(tests, tests => {
|
|
let shouldFlag = true;
|
|
testNavigator.flagTest(tests, shouldFlag);
|
|
})
|
|
}
|
|
|
|
unflag(flag)
|
|
{
|
|
const shouldFlag = false;
|
|
testNavigator.flagTest(Utils.parentOfType(flag, 'tbody'), shouldFlag);
|
|
}
|
|
|
|
visibleTests(opt_container)
|
|
{
|
|
let container = opt_container || document;
|
|
if (this.onlyShowUnexpectedFailures())
|
|
return container.querySelectorAll('tbody:not(.expected)');
|
|
else
|
|
return container.querySelectorAll('tbody');
|
|
}
|
|
|
|
// FIXME: this is confusing. Flip the sense around.
|
|
onlyShowUnexpectedFailures()
|
|
{
|
|
return document.getElementById('unexpected-results').checked;
|
|
}
|
|
|
|
static _testListHeader(title)
|
|
{
|
|
let header = document.createElement('h1');
|
|
header.innerHTML = title + ' (<span class=test-list-count></span>): <a href="#" class=flag-all onclick="controller.flagAll(this)">flag all</a>';
|
|
return header;
|
|
}
|
|
|
|
testToURL(testResult, layoutTestsPath)
|
|
{
|
|
const mappings = {
|
|
"http/tests/ssl/": "https://127.0.0.1:8443/ssl/",
|
|
"http/tests/": "http://127.0.0.1:8000/",
|
|
"http/wpt/": "http://localhost:8800/WebKit/",
|
|
"imported/w3c/web-platform-tests/": "http://localhost:8800/"
|
|
};
|
|
|
|
for (let key in mappings) {
|
|
if (testResult.name.startsWith(key))
|
|
return mappings[key] + testResult.name.substring(key.length);
|
|
|
|
}
|
|
return "file://" + layoutTestsPath + "/" + testResult.name;
|
|
}
|
|
|
|
layoutTestURL(testResult)
|
|
{
|
|
if (this.shouldUseTracLinks())
|
|
return this.layoutTestsBasePath() + testResult.name;
|
|
|
|
return this.testToURL(testResult, this.layoutTestsBasePath());
|
|
}
|
|
|
|
layoutTestsBasePath()
|
|
{
|
|
let basePath;
|
|
if (this.shouldUseTracLinks()) {
|
|
let revision = this.testResults.revision;
|
|
basePath = 'http://trac.webkit.org';
|
|
basePath += revision ? ('/export/' + revision) : '/browser';
|
|
basePath += '/trunk/LayoutTests/';
|
|
} else
|
|
basePath = this.testResults.layoutTestsDir() + '/';
|
|
|
|
return basePath;
|
|
}
|
|
|
|
convertToLayoutTestBaseRelativeURL(fullURL)
|
|
{
|
|
if (fullURL.startsWith('file://')) {
|
|
let urlPrefix = 'file://' + this.layoutTestsBasePath();
|
|
if (fullURL.startsWith(urlPrefix))
|
|
return fullURL.substring(urlPrefix.length);
|
|
}
|
|
|
|
return fullURL;
|
|
}
|
|
|
|
shouldUseTracLinks()
|
|
{
|
|
return !this.testResults.layoutTestsDir() || !location.toString().indexOf('file://') == 0;
|
|
}
|
|
|
|
checkServerIsRunning(event)
|
|
{
|
|
if (this.shouldUseTracLinks())
|
|
return;
|
|
|
|
let url = event.target.href;
|
|
if (url.startsWith("file://"))
|
|
return;
|
|
|
|
event.preventDefault();
|
|
fetch(url, { mode: "no-cors" }).then(() => {
|
|
window.location = url;
|
|
}, () => {
|
|
alert("HTTP server does not seem to be running, please use the run-webkit-httpd script");
|
|
});
|
|
}
|
|
|
|
testLink(testResult)
|
|
{
|
|
return '<a class=test-link onclick="controller.checkServerIsRunning(event)" href="' + this.layoutTestURL(testResult) + '">' + testResult.name + '</a><span class=flag onclick="controller.unflag(this)"> \u2691</span>';
|
|
}
|
|
|
|
expandButtonSpan()
|
|
{
|
|
return '<span class=expand-button onclick="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>';
|
|
}
|
|
|
|
static resultLink(testPrefix, suffix, contents)
|
|
{
|
|
return '<a class=result-link href="' + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> ';
|
|
}
|
|
|
|
textResultLinks(prefix)
|
|
{
|
|
let html = TestResultsController.resultLink(prefix, '-expected.txt', 'expected') +
|
|
TestResultsController.resultLink(prefix, '-actual.txt', 'actual') +
|
|
TestResultsController.resultLink(prefix, '-diff.txt', 'diff');
|
|
|
|
if (this.testResults.hasPrettyPatch())
|
|
html += TestResultsController.resultLink(prefix, '-pretty-diff.html', 'pretty diff');
|
|
|
|
if (this.testResults.hasWDiff())
|
|
html += TestResultsController.resultLink(prefix, '-wdiff.html', 'wdiff');
|
|
|
|
return html;
|
|
}
|
|
|
|
flakinessDashboardURLForTests(testObjects)
|
|
{
|
|
// FIXME: just map and join here.
|
|
let testList = '';
|
|
for (let i = 0; i < testObjects.length; ++i) {
|
|
testList += testObjects[i].name;
|
|
|
|
if (i != testObjects.length - 1)
|
|
testList += ',';
|
|
}
|
|
|
|
return 'https://results.webkit.org/?suite=layout-tests&test=' + encodeURIComponent(testList);
|
|
}
|
|
|
|
_updatePageTitle()
|
|
{
|
|
let dateString = this.testResults.date();
|
|
let title = document.createElement('title');
|
|
title.textContent = 'Layout Test Results from ' + dateString;
|
|
document.head.appendChild(title);
|
|
}
|
|
|
|
// Options handling. FIXME: move to a separate class?
|
|
updateAllOptions()
|
|
{
|
|
Utils.forEach(document.querySelectorAll('#options-menu input'), input => { input.onchange() });
|
|
}
|
|
|
|
toggleOptionsMenu()
|
|
{
|
|
let menu = document.getElementById('options-menu');
|
|
menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu';
|
|
}
|
|
|
|
handleToggleUseNewlines()
|
|
{
|
|
OptionWriter.save();
|
|
testNavigator.updateFlaggedTests();
|
|
}
|
|
|
|
handleUnexpectedResultsChange()
|
|
{
|
|
OptionWriter.save();
|
|
this._updateExpectedFailures();
|
|
}
|
|
|
|
expandAllExpectations()
|
|
{
|
|
let expandLinks = this._visibleExpandLinks();
|
|
for (let link of expandLinks)
|
|
Utils.async(link => { controller.expandExpectations(link) }, [ link ]);
|
|
}
|
|
|
|
collapseAllExpectations()
|
|
{
|
|
let expandLinks = this._visibleExpandLinks();
|
|
for (let link of expandLinks)
|
|
Utils.async(link => { controller.collapseExpectations(link) }, [ link ]);
|
|
}
|
|
|
|
expandExpectations(expandLink)
|
|
{
|
|
let row = Utils.parentOfType(expandLink, 'tr');
|
|
let parentTbody = row.parentNode;
|
|
let existingResultsRow = parentTbody.querySelector('.results-row');
|
|
|
|
const enDash = '\u2013';
|
|
expandLink.textContent = enDash;
|
|
if (existingResultsRow) {
|
|
this._updateExpandedState(existingResultsRow, true);
|
|
return;
|
|
}
|
|
|
|
let testName = row.getAttribute('data-test-name');
|
|
let testResult = this.testResults.testWithName(testName);
|
|
|
|
let newRow = TestResultsController._buildExpandedRowForTest(testResult, row);
|
|
parentTbody.appendChild(newRow);
|
|
|
|
this._updateExpandedState(newRow, true);
|
|
|
|
this._updateImageTogglingTimer();
|
|
}
|
|
|
|
collapseExpectations(expandLink)
|
|
{
|
|
expandLink.textContent = '+';
|
|
let existingResultsRow = Utils.parentOfType(expandLink, 'tbody').querySelector('.results-row');
|
|
if (existingResultsRow)
|
|
this._updateExpandedState(existingResultsRow, false);
|
|
}
|
|
|
|
toggleExpectations(element)
|
|
{
|
|
let expandLink = element;
|
|
if (expandLink.className != 'expand-button-text')
|
|
expandLink = expandLink.querySelector('.expand-button-text');
|
|
|
|
if (expandLink.textContent == '+')
|
|
this.expandExpectations(expandLink);
|
|
else
|
|
this.collapseExpectations(expandLink);
|
|
}
|
|
|
|
_updateExpandedState(row, isExpanded)
|
|
{
|
|
row.setAttribute('data-expanded', isExpanded);
|
|
this._updateImageTogglingTimer();
|
|
}
|
|
|
|
handleToggleImagesChange()
|
|
{
|
|
OptionWriter.save();
|
|
this._updateTogglingImages();
|
|
}
|
|
|
|
_visibleExpandLinks()
|
|
{
|
|
if (this.onlyShowUnexpectedFailures())
|
|
return document.querySelectorAll('tbody:not(.expected) .expand-button-text');
|
|
else
|
|
return document.querySelectorAll('.expand-button-text');
|
|
}
|
|
|
|
static _togglingImage(prefix)
|
|
{
|
|
return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' + prefix + '"></img></div>';
|
|
}
|
|
|
|
_updateTogglingImages()
|
|
{
|
|
this.shouldToggleImages = document.getElementById('toggle-images').checked;
|
|
|
|
// FIXME: this is all pretty confusing. Simplify.
|
|
if (this.shouldToggleImages) {
|
|
Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) a[href$=".png"]'), TestResultsController._convertToTogglingHandler(function(prefix) {
|
|
return TestResultsController.resultLink(prefix, '-diffs.html', 'images');
|
|
}));
|
|
Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) img[src$=".png"]'), TestResultsController._convertToTogglingHandler(TestResultsController._togglingImage));
|
|
} else {
|
|
Utils.forEach(document.querySelectorAll('a[href$="-diffs.html"]'), element => {
|
|
TestResultsController._convertToNonTogglingHandler(element);
|
|
});
|
|
Utils.forEach(document.querySelectorAll('.animatedImage'), TestResultsController._convertToNonTogglingHandler(function (absolutePrefix, suffix) {
|
|
return TestResultsController._resultIframe(absolutePrefix + suffix);
|
|
}));
|
|
}
|
|
|
|
this._updateImageTogglingTimer();
|
|
}
|
|
|
|
_updateExpectedFailures()
|
|
{
|
|
// Gross to do this by setting stylesheet text. Use a body class!
|
|
document.getElementById('unexpected-style').textContent = this.onlyShowUnexpectedFailures() ? '.expected { display: none; }' : '';
|
|
|
|
this.updateTestlistCounts();
|
|
testNavigator.onlyShowUnexpectedFailuresChanged();
|
|
}
|
|
|
|
static _buildExpandedRowForTest(testResult, row)
|
|
{
|
|
let newRow = document.createElement('tr');
|
|
newRow.className = 'results-row';
|
|
let newCell = document.createElement('td');
|
|
newCell.colSpan = row.querySelectorAll('td').length;
|
|
|
|
// FIXME: migrate more of code to using testResult for building the expanded content.
|
|
let resultLinks = row.querySelectorAll('.result-link');
|
|
let hasTogglingImages = false;
|
|
for (let link of resultLinks) {
|
|
let result;
|
|
if (link.textContent == 'images') {
|
|
hasTogglingImages = true;
|
|
result = TestResultsController._togglingImage(link.getAttribute('data-prefix'));
|
|
} else
|
|
result = TestResultsController._resultIframe(link.href);
|
|
|
|
Utils.appendHTML(newCell, result);
|
|
}
|
|
|
|
if (testResult.hasLeak())
|
|
newCell.appendChild(TestResultsController._makeLeaksCell(testResult));
|
|
|
|
newRow.appendChild(newCell);
|
|
|
|
return newRow;
|
|
}
|
|
|
|
static _makeLeaksCell(testResult)
|
|
{
|
|
let container = document.createElement('div');
|
|
container.className = 'result-container leaks';
|
|
|
|
let label = document.createElement('div');
|
|
label.className = 'label';
|
|
label.textContent = "Leaks";
|
|
container.appendChild(label);
|
|
|
|
let leaksTable = document.createElement('table');
|
|
|
|
for (let leak of testResult.info.leaks) {
|
|
let leakRow = document.createElement('tr');
|
|
|
|
for (let leakedObjectType in leak) {
|
|
let th = document.createElement('th');
|
|
th.textContent = leakedObjectType;
|
|
let td = document.createElement('td');
|
|
|
|
let url = leak[leakedObjectType]; // FIXME: when we track leaks other than documents, this might not be a URL.
|
|
td.textContent = controller.convertToLayoutTestBaseRelativeURL(url)
|
|
|
|
leakRow.appendChild(th);
|
|
leakRow.appendChild(td);
|
|
}
|
|
|
|
leaksTable.appendChild(leakRow);
|
|
}
|
|
|
|
container.appendChild(leaksTable);
|
|
return container;
|
|
}
|
|
|
|
static _resultIframe(src)
|
|
{
|
|
// FIXME: use audio tags for AUDIO tests?
|
|
let layoutTestsIndex = src.indexOf('LayoutTests');
|
|
let name;
|
|
if (layoutTestsIndex != -1) {
|
|
let hasTrac = src.indexOf('trac.webkit.org') != -1;
|
|
let prefix = hasTrac ? 'trac.webkit.org/.../' : '';
|
|
name = prefix + src.substring(layoutTestsIndex + 'LayoutTests/'.length);
|
|
} else {
|
|
let lastDashIndex = src.lastIndexOf('-pretty');
|
|
if (lastDashIndex == -1)
|
|
lastDashIndex = src.lastIndexOf('-');
|
|
name = src.substring(lastDashIndex + 1);
|
|
}
|
|
|
|
let tagName = (src.lastIndexOf('.png') == -1) ? 'iframe' : 'img';
|
|
|
|
if (tagName != 'img')
|
|
src += '?format=txt';
|
|
return '<div class=result-container><div class=label>' + name + '</div><' + tagName + ' src="' + src + '"></' + tagName + '></div>';
|
|
}
|
|
|
|
|
|
static _toggleImages()
|
|
{
|
|
let images = document.querySelectorAll('.animatedImage');
|
|
let imageTexts = document.querySelectorAll('.imageText');
|
|
for (let i = 0, len = images.length; i < len; i++) {
|
|
let image = images[i];
|
|
let text = imageTexts[i];
|
|
if (text.textContent == 'Expected Image') {
|
|
text.textContent = 'Actual Image';
|
|
image.src = image.getAttribute('data-prefix') + '-actual.png';
|
|
} else {
|
|
text.textContent = 'Expected Image';
|
|
image.src = image.getAttribute('data-prefix') + '-expected.png';
|
|
}
|
|
}
|
|
}
|
|
|
|
_updateImageTogglingTimer()
|
|
{
|
|
let hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage');
|
|
if (!hasVisibleAnimatedImage) {
|
|
clearInterval(this._togglingImageInterval);
|
|
this._togglingImageInterval = null;
|
|
return;
|
|
}
|
|
|
|
if (!this._togglingImageInterval) {
|
|
TestResultsController._toggleImages();
|
|
this._togglingImageInterval = setInterval(TestResultsController._toggleImages, 2000);
|
|
}
|
|
}
|
|
|
|
static _getResultContainer(node)
|
|
{
|
|
return (node.tagName == 'IMG') ? Utils.parentOfType(node, '.result-container') : node;
|
|
}
|
|
|
|
static _convertToTogglingHandler(togglingImageFunction)
|
|
{
|
|
return function(node) {
|
|
let url = (node.tagName == 'IMG') ? node.src : node.href;
|
|
if (url.match('-expected.png$'))
|
|
TestResultsController._getResultContainer(node).remove();
|
|
else if (url.match('-actual.png$')) {
|
|
let name = Utils.parentOfType(node, 'tbody').querySelector('.test-link').textContent;
|
|
TestResultsController._getResultContainer(node).outerHTML = togglingImageFunction(Utils.stripExtension(name));
|
|
}
|
|
}
|
|
}
|
|
|
|
static _convertToNonTogglingHandler(resultFunction)
|
|
{
|
|
return function(node) {
|
|
let prefix = node.getAttribute('data-prefix');
|
|
TestResultsController._getResultContainer(node).outerHTML = resultFunction(prefix, '-expected.png', 'expected') + resultFunction(prefix, '-actual.png', 'actual');
|
|
}
|
|
}
|
|
};
|
|
|
|
class SectionBuilder {
|
|
|
|
constructor(tests, tableID, resultsController)
|
|
{
|
|
this._tests = tests;
|
|
this._table = null;
|
|
this._resultsController = resultsController;
|
|
this._tableID = tableID;
|
|
}
|
|
|
|
build()
|
|
{
|
|
TestResults.sortByName(this._tests);
|
|
|
|
let section = document.createElement('section');
|
|
section.appendChild(TestResultsController._testListHeader(this.sectionTitle()));
|
|
if (this.hideWhenShowingUnexpectedResultsOnly())
|
|
section.classList.add('expected');
|
|
|
|
this._table = document.createElement('table');
|
|
this._table.id = this.tableID();
|
|
this.addTableHeader();
|
|
|
|
let visibleResultsCount = 0;
|
|
for (let testResult of this._tests) {
|
|
let tbody = this.createTableRow(testResult);
|
|
this._table.appendChild(tbody);
|
|
|
|
if (!this._resultsController.onlyShowUnexpectedFailures() || testResult.isExpected)
|
|
++visibleResultsCount;
|
|
}
|
|
|
|
section.querySelector('.test-list-count').textContent = visibleResultsCount;
|
|
section.appendChild(this._table);
|
|
return section;
|
|
}
|
|
|
|
createTableRow(testResult)
|
|
{
|
|
let tbody = document.createElement('tbody');
|
|
if (testResult.isExpected)
|
|
tbody.classList.add('expected');
|
|
|
|
let row = document.createElement('tr');
|
|
row.setAttribute('data-test-name', testResult.name);
|
|
tbody.appendChild(row);
|
|
|
|
let testNameCell = document.createElement('td');
|
|
this.fillTestCell(testResult, testNameCell);
|
|
row.appendChild(testNameCell);
|
|
|
|
let resultCell = document.createElement('td');
|
|
this.fillTestResultCell(testResult, resultCell);
|
|
row.appendChild(resultCell);
|
|
|
|
let historyCell = this.createHistoryCell(testResult);
|
|
if (historyCell)
|
|
row.appendChild(historyCell);
|
|
|
|
return tbody;
|
|
}
|
|
|
|
hideWhenShowingUnexpectedResultsOnly()
|
|
{
|
|
return !TestResults.hasUnexpectedResult(this._tests);
|
|
}
|
|
|
|
addTableHeader()
|
|
{
|
|
}
|
|
|
|
fillTestCell(testResult, cell)
|
|
{
|
|
let testLink = this.linkifyTestName() ? this._resultsController.testLink(testResult) : testResult.name;
|
|
if (this.rowsAreExpandable()) {
|
|
cell.innerHTML = this._resultsController.expandButtonSpan() + testLink;
|
|
return;
|
|
}
|
|
|
|
cell.innerHTML = testLink;
|
|
}
|
|
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
}
|
|
|
|
createHistoryCell(testResult)
|
|
{
|
|
let historyCell = document.createElement('td');
|
|
historyCell.innerHTML = '<a href="' + this._resultsController.flakinessDashboardURLForTests([testResult]) + '">history</a>'
|
|
return historyCell;
|
|
}
|
|
|
|
tableID()
|
|
{
|
|
return this._tableID;
|
|
}
|
|
|
|
rowsAreExpandable()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
linkifyTestName()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
sectionTitle() { return ''; }
|
|
};
|
|
|
|
class FailuresSectionBuilder extends SectionBuilder {
|
|
|
|
addTableHeader()
|
|
{
|
|
let header = document.createElement('thead');
|
|
let html = '<th>test</th><th id="text-results-header">results</th><th id="image-results-header">image results</th>';
|
|
|
|
if (this._resultsController.testResults.usesExpectationsFile())
|
|
html += '<th>actual failure</th><th>expected failure</th>';
|
|
|
|
html += '<th><a href="' + this._resultsController.flakinessDashboardURLForTests(this._tests) + '">history</a></th>';
|
|
|
|
if (this.tableID() == 'flaky-tests-table') // FIXME: use the classes, Luke!
|
|
html += '<th>failures</th>';
|
|
|
|
header.innerHTML = html;
|
|
this._table.appendChild(header);
|
|
}
|
|
|
|
createTableRow(testResult)
|
|
{
|
|
let tbody = document.createElement('tbody');
|
|
if (testResult.isExpected)
|
|
tbody.classList.add('expected');
|
|
|
|
if (testResult.isMismatchRefTest())
|
|
tbody.setAttribute('mismatchreftest', 'true');
|
|
|
|
let row = document.createElement('tr');
|
|
row.setAttribute('data-test-name', testResult.name);
|
|
tbody.appendChild(row);
|
|
|
|
let testNameCell = document.createElement('td');
|
|
this.fillTestCell(testResult, testNameCell);
|
|
row.appendChild(testNameCell);
|
|
|
|
let resultCell = document.createElement('td');
|
|
this.fillTestResultCell(testResult, resultCell);
|
|
row.appendChild(resultCell);
|
|
|
|
if (testResult.isTextFailure())
|
|
this.appendTextFailureLinks(testResult, resultCell);
|
|
|
|
if (testResult.isAudioFailure())
|
|
this.appendAudioFailureLinks(testResult, resultCell);
|
|
|
|
if (testResult.hasMissingResult())
|
|
this.appendActualOnlyLinks(testResult, resultCell);
|
|
|
|
let actualTokens = testResult.info.actual.split(/\s+/);
|
|
|
|
let testPrefix = Utils.stripExtension(testResult.name);
|
|
let imageResults = this.imageResultLinks(testResult, testPrefix, actualTokens[0]);
|
|
if (!imageResults && actualTokens.length > 1)
|
|
imageResults = this.imageResultLinks(testResult, 'retries/' + testPrefix, actualTokens[1]);
|
|
|
|
let imageResultsCell = document.createElement('td');
|
|
imageResultsCell.innerHTML = imageResults;
|
|
row.appendChild(imageResultsCell);
|
|
|
|
if (this._resultsController.testResults.usesExpectationsFile() || actualTokens.length) {
|
|
let actualCell = document.createElement('td');
|
|
actualCell.textContent = testResult.info.actual;
|
|
row.appendChild(actualCell);
|
|
}
|
|
|
|
if (this._resultsController.testResults.usesExpectationsFile()) {
|
|
let expectedCell = document.createElement('td');
|
|
expectedCell.textContent = testResult.hasMissingResult() ? '' : testResult.info.expected;
|
|
row.appendChild(expectedCell);
|
|
}
|
|
|
|
let historyCell = this.createHistoryCell(testResult);
|
|
if (historyCell)
|
|
row.appendChild(historyCell);
|
|
|
|
return tbody;
|
|
}
|
|
|
|
appendTextFailureLinks(testResult, cell)
|
|
{
|
|
cell.innerHTML += this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
|
|
}
|
|
|
|
appendAudioFailureLinks(testResult, cell)
|
|
{
|
|
let prefix = Utils.stripExtension(testResult.name);
|
|
cell.innerHTML += TestResultsController.resultLink(prefix, '-expected.wav', 'expected audio')
|
|
+ TestResultsController.resultLink(prefix, '-actual.wav', 'actual audio')
|
|
+ TestResultsController.resultLink(prefix, '-diff.txt', 'textual diff');
|
|
}
|
|
|
|
appendActualOnlyLinks(testResult, cell)
|
|
{
|
|
let prefix = Utils.stripExtension(testResult.name);
|
|
if (testResult.isMissingAudio())
|
|
cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.wav', 'audio result');
|
|
|
|
if (testResult.isMissingText())
|
|
cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.txt', 'result');
|
|
}
|
|
|
|
imageResultLinks(testResult, testPrefix, resultToken)
|
|
{
|
|
let result = '';
|
|
if (resultToken.indexOf('IMAGE') != -1) {
|
|
let testExtension = Utils.splitExtension(testResult.name)[1];
|
|
|
|
if (testResult.isMismatchRefTest()) {
|
|
result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected-mismatch.' + testExtension, 'ref mismatch');
|
|
result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
|
|
} else {
|
|
if (testResult.isMatchRefTest())
|
|
result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected.' + testExtension, 'reference');
|
|
|
|
if (this._resultsController.shouldToggleImages)
|
|
result += TestResultsController.resultLink(testPrefix, '-diffs.html', 'images');
|
|
else {
|
|
result += TestResultsController.resultLink(testPrefix, '-expected.png', 'expected');
|
|
result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
|
|
}
|
|
|
|
let diff = testResult.info.image_diff_percent;
|
|
result += TestResultsController.resultLink(testPrefix, '-diff.png', 'diff (' + diff + '%)');
|
|
}
|
|
}
|
|
|
|
if (testResult.hasMissingResult() && testResult.isMissingImage())
|
|
result += TestResultsController.resultLink(testPrefix, '-actual.png', 'png result');
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
class FailingTestsSectionBuilder extends FailuresSectionBuilder {
|
|
sectionTitle() { return 'Tests that failed text/pixel/audio diff'; }
|
|
};
|
|
|
|
class TestsWithMissingResultsSectionBuilder extends FailuresSectionBuilder {
|
|
sectionTitle() { return 'Tests that had no expected results (probably new)'; }
|
|
|
|
rowsAreExpandable()
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class FlakyPassTestsSectionBuilder extends FailuresSectionBuilder {
|
|
sectionTitle() { return 'Flaky tests (failed the first run and passed on retry)'; }
|
|
};
|
|
|
|
class UnexpectedPassTestsSectionBuilder extends SectionBuilder {
|
|
sectionTitle() { return 'Tests expected to fail but passed'; }
|
|
|
|
addTableHeader()
|
|
{
|
|
let header = document.createElement('thead');
|
|
header.innerHTML = '<th>test</th><th>expected failure</th><th>history</th>';
|
|
this._table.appendChild(header);
|
|
}
|
|
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
cell.innerHTML = testResult.info.expected;
|
|
}
|
|
|
|
rowsAreExpandable()
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class TestsWithStdErrSectionBuilder extends SectionBuilder {
|
|
sectionTitle() { return 'Tests that had stderr output'; }
|
|
hideWhenShowingUnexpectedResultsOnly() { return false; }
|
|
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-stderr.txt', 'stderr');
|
|
}
|
|
};
|
|
|
|
class TimedOutTestsSectionBuilder extends SectionBuilder {
|
|
sectionTitle() { return 'Tests that timed out'; }
|
|
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
// FIXME: only include timeout actual/diff results here if we actually spit out results for timeout tests.
|
|
cell.innerHTML = this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
|
|
}
|
|
};
|
|
|
|
class CrashingTestsSectionBuilder extends SectionBuilder {
|
|
sectionTitle() { return 'Tests that crashed'; }
|
|
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log')
|
|
+ TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-sample.txt', 'sample');
|
|
}
|
|
};
|
|
|
|
class OtherCrashesSectionBuilder extends SectionBuilder {
|
|
sectionTitle() { return 'Other crashes'; }
|
|
fillTestResultCell(testResult, cell)
|
|
{
|
|
cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log');
|
|
}
|
|
|
|
createHistoryCell(testResult)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
linkifyTestName()
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class PixelZoomer {
|
|
constructor()
|
|
{
|
|
this.showOnDelay = true;
|
|
this._zoomFactor = 6;
|
|
|
|
this._resultWidth = 800;
|
|
this._resultHeight = 600;
|
|
|
|
this._percentX = 0;
|
|
this._percentY = 0;
|
|
|
|
document.addEventListener('mousemove', this, false);
|
|
document.addEventListener('mouseout', this, false);
|
|
}
|
|
|
|
_zoomedResultWidth()
|
|
{
|
|
return this._resultWidth * this._zoomFactor;
|
|
}
|
|
|
|
_zoomedResultHeight()
|
|
{
|
|
return this._resultHeight * this._zoomFactor;
|
|
}
|
|
|
|
_zoomImageContainer(url)
|
|
{
|
|
let container = document.createElement('div');
|
|
container.className = 'zoom-image-container';
|
|
|
|
let title = url.match(/\-([^\-]*)\.png/)[1];
|
|
|
|
let label = document.createElement('div');
|
|
label.className = 'label';
|
|
label.appendChild(document.createTextNode(title));
|
|
container.appendChild(label);
|
|
|
|
let imageContainer = document.createElement('div');
|
|
imageContainer.className = 'scaled-image-container';
|
|
|
|
let image = new Image();
|
|
image.src = url;
|
|
image.style.width = this._zoomedResultWidth() + 'px';
|
|
image.style.height = this._zoomedResultHeight() + 'px';
|
|
image.style.border = '1px solid black';
|
|
imageContainer.appendChild(image);
|
|
container.appendChild(imageContainer);
|
|
|
|
return container;
|
|
}
|
|
|
|
_createContainer(e)
|
|
{
|
|
let tbody = Utils.parentOfType(e.target, 'tbody');
|
|
let row = tbody.querySelector('tr');
|
|
let imageDiffLinks = row.querySelectorAll('a[href$=".png"]');
|
|
|
|
let container = document.createElement('div');
|
|
container.className = 'pixel-zoom-container';
|
|
|
|
let html = '';
|
|
|
|
let togglingImageLink = row.querySelector('a[href$="-diffs.html"]');
|
|
if (togglingImageLink) {
|
|
let prefix = togglingImageLink.getAttribute('data-prefix');
|
|
container.appendChild(this._zoomImageContainer(prefix + '-expected.png'));
|
|
container.appendChild(this._zoomImageContainer(prefix + '-actual.png'));
|
|
}
|
|
|
|
for (let link of imageDiffLinks)
|
|
container.appendChild(this._zoomImageContainer(link.href));
|
|
|
|
document.body.appendChild(container);
|
|
this._drawAll();
|
|
}
|
|
|
|
_draw(imageContainer)
|
|
{
|
|
let image = imageContainer.querySelector('img');
|
|
let containerBounds = imageContainer.getBoundingClientRect();
|
|
image.style.left = (containerBounds.width / 2 - this._percentX * this._zoomedResultWidth()) + 'px';
|
|
image.style.top = (containerBounds.height / 2 - this._percentY * this._zoomedResultHeight()) + 'px';
|
|
}
|
|
|
|
_drawAll()
|
|
{
|
|
Utils.forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), element => { this._draw(element) });
|
|
}
|
|
|
|
handleEvent(event)
|
|
{
|
|
if (event.type == 'mousemove') {
|
|
this._handleMouseMove(event);
|
|
return;
|
|
}
|
|
|
|
if (event.type == 'mouseout') {
|
|
this._handleMouseOut(event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_handleMouseOut(event)
|
|
{
|
|
if (event.relatedTarget && event.relatedTarget.tagName != 'IFRAME')
|
|
return;
|
|
|
|
// If e.relatedTarget is null, we've moused out of the document.
|
|
let container = document.querySelector('.pixel-zoom-container');
|
|
if (container)
|
|
container.remove();
|
|
}
|
|
|
|
_handleMouseMove(event)
|
|
{
|
|
if (this._mouseMoveTimeout) {
|
|
clearTimeout(this._mouseMoveTimeout);
|
|
this._mouseMoveTimeout = 0;
|
|
}
|
|
|
|
if (Utils.parentOfType(event.target, '.pixel-zoom-container'))
|
|
return;
|
|
|
|
let container = document.querySelector('.pixel-zoom-container');
|
|
|
|
let resultContainer = (event.target.className == 'result-container') ? event.target : Utils.parentOfType(event.target, '.result-container');
|
|
if (!resultContainer || !resultContainer.querySelector('img')) {
|
|
if (container)
|
|
container.remove();
|
|
return;
|
|
}
|
|
|
|
let targetLocation = event.target.getBoundingClientRect();
|
|
this._percentX = (event.clientX - targetLocation.left) / targetLocation.width;
|
|
this._percentY = (event.clientY - targetLocation.top) / targetLocation.height;
|
|
|
|
if (!container) {
|
|
if (this.showOnDelay) {
|
|
this._mouseMoveTimeout = setTimeout(() => {
|
|
this._createContainer(event);
|
|
}, 400);
|
|
return;
|
|
}
|
|
|
|
this._createContainer(event);
|
|
return;
|
|
}
|
|
|
|
this._drawAll();
|
|
}
|
|
};
|
|
|
|
class TableSorter
|
|
{
|
|
static _forwardArrow()
|
|
{
|
|
return '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>';
|
|
}
|
|
|
|
static _backwardArrow()
|
|
{
|
|
return '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>';
|
|
}
|
|
|
|
static _sortedContents(header, arrow)
|
|
{
|
|
return arrow + ' ' + Utils.trim(header.textContent) + ' ' + arrow;
|
|
}
|
|
|
|
static _updateHeaderClassNames(newHeader)
|
|
{
|
|
let sortHeader = document.querySelector('.sortHeader');
|
|
if (sortHeader) {
|
|
if (sortHeader == newHeader) {
|
|
let isAlreadyReversed = sortHeader.classList.contains('reversed');
|
|
if (isAlreadyReversed)
|
|
sortHeader.classList.remove('reversed');
|
|
else
|
|
sortHeader.classList.add('reversed');
|
|
} else {
|
|
sortHeader.textContent = sortHeader.textContent;
|
|
sortHeader.classList.remove('sortHeader');
|
|
sortHeader.classList.remove('reversed');
|
|
}
|
|
}
|
|
|
|
newHeader.classList.add('sortHeader');
|
|
}
|
|
|
|
static _textContent(tbodyRow, column)
|
|
{
|
|
return tbodyRow.querySelectorAll('td')[column].textContent;
|
|
}
|
|
|
|
static _sortRows(newHeader, reversed)
|
|
{
|
|
let testsTable = document.getElementById('results-table');
|
|
let headers = Utils.toArray(testsTable.querySelectorAll('th'));
|
|
let sortColumn = headers.indexOf(newHeader);
|
|
|
|
let rows = Utils.toArray(testsTable.querySelectorAll('tbody'));
|
|
|
|
rows.sort(function(a, b) {
|
|
// Only need to support lexicographic sort for now.
|
|
let aText = TableSorter._textContent(a, sortColumn);
|
|
let bText = TableSorter._textContent(b, sortColumn);
|
|
|
|
// Forward sort equal values by test name.
|
|
if (sortColumn && aText == bText) {
|
|
let aTestName = TableSorter._textContent(a, 0);
|
|
let bTestName = TableSorter._textContent(b, 0);
|
|
if (aTestName == bTestName)
|
|
return 0;
|
|
return aTestName < bTestName ? -1 : 1;
|
|
}
|
|
|
|
if (reversed)
|
|
return aText < bText ? 1 : -1;
|
|
else
|
|
return aText < bText ? -1 : 1;
|
|
});
|
|
|
|
for (let row of rows)
|
|
testsTable.appendChild(row);
|
|
}
|
|
|
|
static sortColumn(columnNumber)
|
|
{
|
|
let newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber];
|
|
TableSorter._sort(newHeader);
|
|
}
|
|
|
|
static handleClick(e)
|
|
{
|
|
let newHeader = e.target;
|
|
if (newHeader.localName != 'th')
|
|
return;
|
|
TableSorter._sort(newHeader);
|
|
}
|
|
|
|
static _sort(newHeader)
|
|
{
|
|
TableSorter._updateHeaderClassNames(newHeader);
|
|
|
|
let reversed = newHeader.classList.contains('reversed');
|
|
let sortArrow = reversed ? TableSorter._backwardArrow() : TableSorter._forwardArrow();
|
|
newHeader.innerHTML = TableSorter._sortedContents(newHeader, sortArrow);
|
|
|
|
TableSorter._sortRows(newHeader, reversed);
|
|
}
|
|
};
|
|
|
|
class OptionWriter {
|
|
static save()
|
|
{
|
|
let options = document.querySelectorAll('label input');
|
|
let data = {};
|
|
for (let option of options)
|
|
data[option.id] = option.checked;
|
|
|
|
try {
|
|
localStorage.setItem(OptionWriter._key, JSON.stringify(data));
|
|
} catch (err) {
|
|
if (err.name != "SecurityError")
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
static apply()
|
|
{
|
|
let json;
|
|
try {
|
|
json = localStorage.getItem(OptionWriter._key);
|
|
} catch (err) {
|
|
if (err.name != "SecurityError")
|
|
throw err;
|
|
}
|
|
|
|
if (!json) {
|
|
controller.updateAllOptions();
|
|
return;
|
|
}
|
|
|
|
let data = JSON.parse(json);
|
|
for (let id in data) {
|
|
let input = document.getElementById(id);
|
|
if (input)
|
|
input.checked = data[id];
|
|
}
|
|
controller.updateAllOptions();
|
|
}
|
|
|
|
static get _key()
|
|
{
|
|
return 'run-webkit-tests-options';
|
|
}
|
|
};
|
|
|
|
let testResults;
|
|
function ADD_RESULTS(input)
|
|
{
|
|
testResults = new TestResults(input);
|
|
}
|
|
</script>
|
|
|
|
<script src="full_results.json"></script>
|
|
|
|
<script>
|
|
|
|
class TestNavigator
|
|
{
|
|
constructor() {
|
|
this.currentTestIndex = -1;
|
|
this.flaggedTests = {};
|
|
document.addEventListener('keypress', this, false);
|
|
}
|
|
|
|
handleEvent(event)
|
|
{
|
|
if (event.type == 'keypress') {
|
|
this.handleKeyEvent(event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
handleKeyEvent(event)
|
|
{
|
|
if (event.metaKey || event.shiftKey || event.ctrlKey)
|
|
return;
|
|
|
|
switch (String.fromCharCode(event.charCode)) {
|
|
case 'i':
|
|
this._scrollToFirstTest();
|
|
break;
|
|
case 'j':
|
|
this._scrollToNextTest();
|
|
break;
|
|
case 'k':
|
|
this._scrollToPreviousTest();
|
|
break;
|
|
case 'l':
|
|
this._scrollToLastTest();
|
|
break;
|
|
case 'e':
|
|
this._expandCurrentTest();
|
|
break;
|
|
case 'c':
|
|
this._collapseCurrentTest();
|
|
break;
|
|
case 't':
|
|
this._toggleCurrentTest();
|
|
break;
|
|
case 'f':
|
|
this._toggleCurrentTestFlagged();
|
|
break;
|
|
}
|
|
}
|
|
|
|
_scrollToFirstTest()
|
|
{
|
|
if (this._setCurrentTest(0))
|
|
this._scrollToCurrentTest();
|
|
}
|
|
|
|
_scrollToLastTest()
|
|
{
|
|
let links = controller.visibleTests();
|
|
if (this._setCurrentTest(links.length - 1))
|
|
this._scrollToCurrentTest();
|
|
}
|
|
|
|
_scrollToNextTest()
|
|
{
|
|
if (this.currentTestIndex == -1)
|
|
this._scrollToFirstTest();
|
|
else if (this._setCurrentTest(this.currentTestIndex + 1))
|
|
this._scrollToCurrentTest();
|
|
}
|
|
|
|
_scrollToPreviousTest()
|
|
{
|
|
if (this.currentTestIndex == -1)
|
|
this._scrollToLastTest();
|
|
else if (this._setCurrentTest(this.currentTestIndex - 1))
|
|
this._scrollToCurrentTest();
|
|
}
|
|
|
|
_currentTestLink()
|
|
{
|
|
let links = controller.visibleTests();
|
|
return links[this.currentTestIndex];
|
|
}
|
|
|
|
_currentTestExpandLink()
|
|
{
|
|
return this._currentTestLink().querySelector('.expand-button-text');
|
|
}
|
|
|
|
_expandCurrentTest()
|
|
{
|
|
controller.expandExpectations(this._currentTestExpandLink());
|
|
}
|
|
|
|
_collapseCurrentTest()
|
|
{
|
|
controller.collapseExpectations(this._currentTestExpandLink());
|
|
}
|
|
|
|
_toggleCurrentTest()
|
|
{
|
|
controller.toggleExpectations(this._currentTestExpandLink());
|
|
}
|
|
|
|
_toggleCurrentTestFlagged()
|
|
{
|
|
let testLink = this._currentTestLink();
|
|
this.flagTest(testLink, !testLink.classList.contains('flagged'));
|
|
}
|
|
|
|
// FIXME: Test navigator shouldn't know anything about flagging. It should probably call out to TestFlagger or something.
|
|
// FIXME: Batch flagging (avoid updateFlaggedTests on each test).
|
|
flagTest(testTbody, shouldFlag)
|
|
{
|
|
let testName = testTbody.querySelector('.test-link').innerText;
|
|
|
|
if (shouldFlag) {
|
|
testTbody.classList.add('flagged');
|
|
this.flaggedTests[testName] = 1;
|
|
} else {
|
|
testTbody.classList.remove('flagged');
|
|
delete this.flaggedTests[testName];
|
|
}
|
|
|
|
this.updateFlaggedTests();
|
|
}
|
|
|
|
updateFlaggedTests()
|
|
{
|
|
let flaggedTestTextbox = document.getElementById('flagged-tests');
|
|
if (!flaggedTestTextbox) {
|
|
let flaggedTestContainer = document.createElement('div');
|
|
flaggedTestContainer.id = 'flagged-test-container';
|
|
flaggedTestContainer.className = 'floating-panel';
|
|
flaggedTestContainer.innerHTML = '<h2>Flagged Tests</h2><pre id="flagged-tests" contentEditable></pre>';
|
|
document.body.appendChild(flaggedTestContainer);
|
|
|
|
flaggedTestTextbox = document.getElementById('flagged-tests');
|
|
}
|
|
|
|
let flaggedTests = Object.keys(this.flaggedTests);
|
|
flaggedTests.sort();
|
|
let separator = document.getElementById('use-newlines').checked ? '\n' : ' ';
|
|
flaggedTestTextbox.innerHTML = flaggedTests.join(separator);
|
|
document.getElementById('flagged-test-container').style.display = flaggedTests.length ? '' : 'none';
|
|
}
|
|
|
|
_setCurrentTest(testIndex)
|
|
{
|
|
let links = controller.visibleTests();
|
|
if (testIndex < 0 || testIndex >= links.length)
|
|
return false;
|
|
|
|
let currentTest = links[this.currentTestIndex];
|
|
if (currentTest)
|
|
currentTest.classList.remove('current');
|
|
|
|
this.currentTestIndex = testIndex;
|
|
|
|
currentTest = links[this.currentTestIndex];
|
|
currentTest.classList.add('current');
|
|
|
|
return true;
|
|
}
|
|
|
|
_scrollToCurrentTest()
|
|
{
|
|
let targetLink = this._currentTestLink();
|
|
if (!targetLink)
|
|
return;
|
|
|
|
let rowRect = targetLink.getBoundingClientRect();
|
|
// rowRect is in client coords (i.e. relative to viewport), so we just want to add its top to the current scroll position.
|
|
document.scrollingElement.scrollTop += rowRect.top;
|
|
}
|
|
|
|
onlyShowUnexpectedFailuresChanged()
|
|
{
|
|
let currentTest = document.querySelector('.current');
|
|
if (!currentTest)
|
|
return;
|
|
|
|
// If our currentTest became hidden, reset the currentTestIndex.
|
|
if (controller.onlyShowUnexpectedFailures() && currentTest.classList.contains('expected'))
|
|
this._scrollToFirstTest();
|
|
else {
|
|
// Recompute this.currentTestIndex
|
|
let links = controller.visibleTests();
|
|
this.currentTestIndex = links.indexOf(currentTest);
|
|
}
|
|
}
|
|
};
|
|
|
|
function handleMouseDown(e)
|
|
{
|
|
if (!Utils.parentOfType(e.target, '#options-menu') && e.target.id != 'options-link')
|
|
document.getElementById('options-menu').className = 'hidden-menu';
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleMouseDown, false);
|
|
|
|
let controller;
|
|
let pixelZoomer;
|
|
let testNavigator;
|
|
|
|
function generatePage()
|
|
{
|
|
let container = document.getElementById('main-content');
|
|
|
|
controller = new TestResultsController(container, testResults);
|
|
pixelZoomer = new PixelZoomer();
|
|
testNavigator = new TestNavigator();
|
|
|
|
OptionWriter.apply();
|
|
|
|
console.log("rendered test results page!");
|
|
}
|
|
|
|
window.addEventListener('load', generatePage, false);
|
|
|
|
</script>
|
|
<body>
|
|
|
|
<div class="content-container">
|
|
<div id="toolbar" class="floating-panel">
|
|
<div class="note">Use the i, j, k and l keys to navigate, e, c to expand and collapse, and f to flag</div>
|
|
<a class="clickable" onclick="controller.expandAllExpectations()">expand all</a>
|
|
<a class="clickable" onclick="controller.collapseAllExpectations()">collapse all</a>
|
|
<a class="clickable" id=options-link onclick="controller.toggleOptionsMenu()">options</a>
|
|
<div id="options-menu" class="hidden-menu">
|
|
<label><input id="unexpected-results" type="checkbox" checked onchange="controller.handleUnexpectedResultsChange()">Only unexpected results</label>
|
|
<label><input id="toggle-images" type="checkbox" checked onchange="controller.handleToggleImagesChange()">Toggle images</label>
|
|
<label title="Use newlines instead of spaces to separate flagged tests"><input id="use-newlines" type="checkbox" checked onchange="controller.handleToggleUseNewlines()">Use newlines in flagged list</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="main-content"></div>
|
|
|
|
</body>
|