haikuwebkit/ManualTests/resources/test-results-page.html

2064 lines
60 KiB
HTML
Raw Permalink Normal View History

Lazily create m_windowCloseWatchpoints so we don't mistakenly think we have a frame when re-associating a document to a given cached frame https://bugs.webkit.org/show_bug.cgi?id=221098 <rdar://72894454> Reviewed by Ryosuke Niwa and Mark Lam. .: * ManualTests/dont-create-invalid-watchpoint-when-going-back.html: Added. * ManualTests/resources/empty-text.txt: Added. * ManualTests/resources/full_results.json: Added. * ManualTests/resources/test-results-page.html: Added. Source/JavaScriptCore: * bytecode/AccessCase.cpp: (JSC::AccessCase::commit): * bytecode/Watchpoint.h: (JSC::WatchpointSet::isStillValidOnJSThread const): * runtime/PropertySlot.h: (JSC::PropertySlot::setWatchpointSet): Source/WebCore: There's a scenario when we go back while using the back forward cache, and we re-associate a cached frame with a document, that we were creating an already invalidated "frame cleared" watchpoint. There were a few things we were doing wrong: 1. In JSDOMWindowBase's constructor, we thought that we didn't have a frame, even though we did. It was because we hadn't finished the bookkeeping in `FrameLoader::open(CachedFrameBase& cachedFrame)` that associates a document with a frame. And DOMWindow relies on its document to get its frame. 2. When the watchpoint was invalidated, we were still telling the PropertySlot about it. This was breaking JSC's invariant that these had to be valid watchpoints. This patch resolves: 1. We now lazily create the watchpoint when we first need it. By that time, we would've already been associated with a frame in the above example. 2. We check if the watchpoint is still valid before telling the PropertySlot about it, instead of always assuming it's valid. I wasn't able to get this test to fail in WKTR/DRT, because it seems to be we're crashing when running some JS code from Safari's injected bundle. I've added a manual test instead. * bindings/js/JSDOMWindowBase.cpp: (WebCore::JSDOMWindowBase::JSDOMWindowBase): * bindings/js/JSDOMWindowBase.h: * bindings/js/JSDOMWindowCustom.cpp: (WebCore::JSDOMWindow::getOwnPropertySlot): Canonical link: https://commits.webkit.org/233552@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@272174 268f45cc-cd09-0410-ab3c-d52691b4dbfc
2021-02-01 21:40:55 +00:00
<!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>