WebKit Bugzilla
Attachment 347348 Details for
Bug 188690
: Modernize results.html
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch
bug-188690-20180816212056.patch (text/plain), 98.47 KB, created by
Simon Fraser (smfr)
on 2018-08-16 21:20:57 PDT
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
Simon Fraser (smfr)
Created:
2018-08-16 21:20:57 PDT
Size:
98.47 KB
patch
obsolete
>Subversion Revision: 234973 >diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog >index 750c9f9497d2356872b68d156a7d1ec24a7340ea..81f36fd31772fca9446d51f1ae2522b170190bf0 100644 >--- a/LayoutTests/ChangeLog >+++ b/LayoutTests/ChangeLog >@@ -1,3 +1,21 @@ >+2018-08-16 Simon Fraser <simon.fraser@apple.com> >+ >+ Modernize results.html >+ https://bugs.webkit.org/show_bug.cgi?id=188690 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ results.html, which is used to show layout test results, had some very old-school >+ HTML string building to create the tables of test results, making it hard to hack on. >+ >+ Modernize it, using ES6 classes for the major actors, and using DOM API to build most >+ of the content. >+ >+ The page is functionally the same (other than the addition of a missing 'History" column header). >+ >+ * fast/harness/results-expected.txt: >+ * fast/harness/results.html: >+ > 2018-08-16 Basuke Suzuki <Basuke.Suzuki@sony.com> > > [Curl] Bug fix on deleting cookies when Max-Age is set to zero. >diff --git a/LayoutTests/fast/harness/results-expected.txt b/LayoutTests/fast/harness/results-expected.txt >index 6af71da6f0ced94fee8f2986774e9d92fadf33cd..798066443506c6928172bfbd2be464a44d53fe50 100644 >--- a/LayoutTests/fast/harness/results-expected.txt >+++ b/LayoutTests/fast/harness/results-expected.txt >@@ -5,7 +5,7 @@ Use newlines in flagged list > Tests that crashed (1): flag all > > +http/tests/contentextensions/top-url.html crash log sample history >-Other Crashes (2): flag all >+Other crashes (2): flag all > > +DumpRenderTree-54888 crash log > +DumpRenderTree-56804 crash log >@@ -31,7 +31,7 @@ test results image results actual failure expected failure history failures > +media/video-loop.html expected actual diff pretty diff text pass timeout pass timeout history > Tests expected to fail but passed (4): flag all > >-test expected failure >+test expected failure history > canvas/philip/tests/2d.gradient.interpolate.solid.html fail history > editing/spelling/spelling-marker-includes-hyphen.html image history > editing/spelling/spelling-markers-in-overlapping-lines.html image history >diff --git a/LayoutTests/fast/harness/results.html b/LayoutTests/fast/harness/results.html >index 1077bd0ca127d8ce921f6554d97273b4836dfa40..4a514069b8a3449e525f065974d016b7dafd3377 100644 >--- a/LayoutTests/fast/harness/results.html >+++ b/LayoutTests/fast/harness/results.html >@@ -20,6 +20,12 @@ p { > margin-bottom: 0.3em; > } > >+a.clickable { >+ color: blue; >+ cursor: pointer; >+ margin-left: 0.2em; >+} >+ > tr:not(.results-row) td { > white-space: nowrap; > } >@@ -32,7 +38,7 @@ td:not(:first-of-type) { > text-transform: lowercase; > } > >-td { >+th, td { > padding: 1px 4px; > } > >@@ -75,7 +81,7 @@ th { > } > > .floating-panel { >- padding: 4px; >+ padding: 6px; > background-color: rgba(255, 255, 255, 0.9); > border: 1px solid silver; > border-radius: 4px; >@@ -137,11 +143,12 @@ tbody.flagged .flag { > } > > #options-menu { >- border: 1px solid; >+ border: 1px solid gray; >+ border-radius: 4px; > margin-top: 1px; > padding: 2px 4px; >- box-shadow: 2px 2px 2px #888; >- -webkit-transition: opacity .2s; >+ box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6); >+ transition: opacity .2s; > text-align: left; > position: absolute; > right: 4px; >@@ -229,1301 +236,1674 @@ tbody.flagged .flag { > if (window.testRunner) > testRunner.dumpAsText(); > >-var g_state; >-function globalState() >+class Utils > { >- if (!g_state) { >- g_state = { >- crashTests: [], >- crashOther: [], >- flakyPassTests: [], >- hasHttpTests: false, >- hasImageFailures: false, >- hasTextFailures: false, >- missingResults: [], >- results: {}, >- shouldToggleImages: true, >- failingTests: [], >- testsWithStderr: [], >- timeoutTests: [], >- unexpectedPassTests: [] >+ 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; > } >- return g_state; >-} > >-function ADD_RESULTS(input) >-{ >- globalState().results = input; >-} >-</script> >+ 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]; >+ } > >-<script src="full_results.json"></script> >+ static splitExtension(testName) >+ { >+ let index = testName.lastIndexOf('.'); >+ if (index == -1) { >+ return [testName, '']; >+ } >+ return [testName.substring(0, index), testName.substring(index + 1)]; >+ } > >-<script> >-function splitExtension(test) >-{ >- var index = test.lastIndexOf('.'); >- if (index == -1) { >- return [test, ""]; >+ static forEach(nodeList, handler) >+ { >+ Array.prototype.forEach.call(nodeList, handler); > } >- return [test.substring(0, index), test.substring(index + 1)]; >-} > >-function stripExtension(test) >-{ >- // 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 (splitExtension(test)[1].length > 5) >- return test; >- return splitExtension(test)[0]; >-} >+ static toArray(nodeList) >+ { >+ return Array.prototype.slice.call(nodeList); >+ } > >-function matchesSelector(node, selector) >-{ >- if (node.matches) >- return node.matches(selector); >+ static trim(string) >+ { >+ return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); >+ } > >- if (node.webkitMatchesSelector) >- return node.webkitMatchesSelector(selector); >+ static async(func, args) >+ { >+ setTimeout(() => { func.apply(null, args); }, 50); >+ } > >- if (node.mozMatchesSelector) >- return node.mozMatchesSelector(selector); >-} >+ static appendHTML(node, html) >+ { >+ if (node.insertAdjacentHTML) >+ node.insertAdjacentHTML('beforeEnd', html); >+ else >+ node.innerHTML += html; >+ }}; > >-function parentOfType(node, selector) >+class TestResult > { >- while (node = node.parentNode) { >- if (matchesSelector(node, selector)) >- return node; >+ constructor(info, name) >+ { >+ this.name = name; >+ this.info = info; // FIXME: make this private. > } >- return null; >-} > >-function remove(node) >-{ >- node.parentNode.removeChild(node); >-} >+ 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; >+ } >+ >+ isMissing() >+ { >+ 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'; >+ } > >-function forEach(nodeList, handler) >-{ >- Array.prototype.forEach.call(nodeList, handler); >-} >+ isTextFailure() >+ { >+ return this.info.actual.indexOf('TEXT') != -1; >+ } > >-function resultIframe(src) >-{ >- // FIXME: use audio tags for AUDIO tests? >- var layoutTestsIndex = src.indexOf('LayoutTests'); >- var name; >- if (layoutTestsIndex != -1) { >- var hasTrac = src.indexOf('trac.webkit.org') != -1; >- var prefix = hasTrac ? 'trac.webkit.org/.../' : ''; >- name = prefix + src.substring(layoutTestsIndex + 'LayoutTests/'.length); >- } else { >- var lastDashIndex = src.lastIndexOf('-pretty'); >- if (lastDashIndex == -1) >- lastDashIndex = src.lastIndexOf('-'); >- name = src.substring(lastDashIndex + 1); >- } >- >- var 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>'; >-} >+ isImageFailure() >+ { >+ return this.info.actual.indexOf('IMAGE') != -1; >+ } > >-function togglingImage(prefix) >-{ >- return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' + >- prefix + '"></img></div>'; >-} >+ isAudioFailure() >+ { >+ return this.info.actual.indexOf('AUDIO') != -1; >+ } > >-function toggleExpectations(element) >-{ >- var expandLink = element; >- if (expandLink.className != 'expand-button-text') >- expandLink = expandLink.querySelector('.expand-button-text'); >- >- if (expandLink.textContent == '+') >- expandExpectations(expandLink); >- else >- collapseExpectations(expandLink); >-} >+ 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; >+ } > >-function collapseExpectations(expandLink) >-{ >- expandLink.textContent = '+'; >- var existingResultsRow = parentOfType(expandLink, 'tbody').querySelector('.results-row'); >- if (existingResultsRow) >- updateExpandedState(existingResultsRow, false); >-} >+ isMismatchRefTest() >+ { >+ return this.isRefTest() && this.info.reftest_type.indexOf('!=') != -1; >+ } > >-function updateExpandedState(row, isExpanded) >-{ >- row.setAttribute('data-expanded', isExpanded); >- updateImageTogglingTimer(); >-} >+ isMatchRefTest() >+ { >+ return this.isRefTest() && this.info.reftest_type.indexOf('==') != -1; >+ } >+ >+ isMissingImage() >+ { >+ return this.info.is_missing_image; >+ } >+ >+ hasStdErr() >+ { >+ return this.info.has_stderr; >+ } >+}; > >-function appendHTML(node, html) >+class TestResults > { >- if (node.insertAdjacentHTML) >- node.insertAdjacentHTML('beforeEnd', html); >- else >- node.innerHTML += html; >-} >+ constructor(results) >+ { >+ this._results = results; > >-function expandExpectations(expandLink) >-{ >- var row = parentOfType(expandLink, 'tr'); >- var parentTbody = row.parentNode; >- var existingResultsRow = parentTbody.querySelector('.results-row'); >- >- var enDash = '\u2013'; >- expandLink.textContent = enDash; >- if (existingResultsRow) { >- updateExpandedState(existingResultsRow, true); >- return; >+ 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._forEachTest(this._results.tests, ''); >+ this._forOtherCrashes(this._results.other_crashes); > } > >- var newRow = document.createElement('tr'); >- newRow.className = 'results-row'; >- var newCell = document.createElement('td'); >- newCell.colSpan = row.querySelectorAll('td').length; >- >- var resultLinks = row.querySelectorAll('.result-link'); >- var hasTogglingImages = false; >- for (var i = 0; i < resultLinks.length; i++) { >- var link = resultLinks[i]; >- var result; >- if (link.textContent == 'images') { >- hasTogglingImages = true; >- result = togglingImage(link.getAttribute('data-prefix')); >- } else >- result = resultIframe(link.href); >+ date() >+ { >+ return this._results.date; >+ } > >- appendHTML(newCell, result); >+ 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; > } > >- newRow.appendChild(newCell); >- parentTbody.appendChild(newRow); >+ hasPrettyPatch() >+ { >+ return this._results.has_pretty_patch; >+ } >+ >+ hasWDiff() >+ { >+ return this._results.has_wdiff; >+ } > >- updateExpandedState(newRow, true); >+ _processResultForTest(testResult) >+ { >+ let test = testResult.name; >+ if (testResult.hasStdErr()) >+ this.testsWithStderr.push(testResult); > >- updateImageTogglingTimer(); >-} >+ this.hasHttpTests |= test.indexOf('http/') == 0; > >-function updateImageTogglingTimer() >-{ >- var hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage'); >- if (!hasVisibleAnimatedImage) { >- clearInterval(globalState().togglingImageInterval); >- globalState().togglingImageInterval = null; >- return; >- } >+ if (this.usesExpectationsFile()) >+ testResult.isExpected = testResult.isFailureExpected(); >+ >+ if (testResult.isTextFailure()) >+ this.hasTextFailures = true; > >- if (!globalState().togglingImageInterval) { >- toggleImages(); >- globalState().togglingImageInterval = setInterval(toggleImages, 2000); >- } >-} >+ if (testResult.isImageFailure()) >+ this.hasImageFailures = true; > >-function async(func, args) >-{ >- setTimeout(function() { func.apply(null, args); }, 100); >-} >+ if (testResult.isMissing()) { >+ // FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for tests with MISSING results. >+ this.missingResults.push(testResult); >+ return; >+ } > >-function visibleTests(opt_container) >-{ >- var container = opt_container || document; >- if (onlyShowUnexpectedFailures()) >- return container.querySelectorAll('tbody:not(.expected)'); >- else >- return container.querySelectorAll('tbody'); >-} >+ if (testResult.isFlakey(this._results.pixel_tests_enabled)) { >+ this.flakyPassTests.push(testResult); >+ return; >+ } > >-function visibleExpandLinks() >-{ >- if (onlyShowUnexpectedFailures()) >- return document.querySelectorAll('tbody:not(.expected) .expand-button-text'); >- else >- return document.querySelectorAll('.expand-button-text'); >-} >+ if (testResult.isPass()) { >+ if (testResult.isUnexpectedPass(this._results.pixel_tests_enabled)) >+ this.unexpectedPassTests.push(testResult); >+ return; >+ } > >-function expandAllExpectations() >-{ >- var expandLinks = visibleExpandLinks(); >- for (var i = 0, len = expandLinks.length; i < len; i++) >- async(expandExpectations, [expandLinks[i]]); >-} >+ if (testResult.isCrash()) { >+ this.crashTests.push(testResult); >+ return; >+ } > >-function collapseAllExpectations() >-{ >- var expandLinks = visibleExpandLinks(); >- for (var i = 0, len = expandLinks.length; i < len; i++) >- async(collapseExpectations, [expandLinks[i]]); >-} >+ 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); >+ } >+ } > >-function shouldUseTracLinks() >-{ >- return !globalState().results.layout_tests_dir || !location.toString().indexOf('file://') == 0; >-} >+ _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) }); >+ } > >-function layoutTestsBasePath() >-{ >- var basePath; >- if (shouldUseTracLinks()) { >- var revision = globalState().results.revision; >- basePath = 'http://trac.webkit.org'; >- basePath += revision ? ('/export/' + revision) : '/browser'; >- basePath += '/trunk/LayoutTests/'; >- } else >- basePath = globalState().results.layout_tests_dir + '/'; >- return basePath; >-} >+ static hasUnexpectedResult(tests) >+ { >+ return tests.some(function (test) { return !test.isExpected; }); >+ } >+}; > >-var 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/" >-} >+class TestResultsController >+{ >+ constructor(containerElement, testResults) >+ { >+ this.containerElement = containerElement; >+ this.testResults = testResults; > >-function testToURL(test, layoutTestsPath) >-{ >- for (let key in mappings) { >- if (test.startsWith(key)) >- return mappings[key] + test.substring(key.length); >+ this.shouldToggleImages = true; >+ this._togglingImageInterval = null; >+ >+ this._updatePageTitle(); > >+ this.buildResultsTables(); >+ this.hideNonApplicableUI(); >+ this.setupSorting(); >+ this.setupOptions(); > } >- return "file://" + layoutTestsPath + "/" + test >-} >+ >+ 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); >+ } > >-function layoutTestURL(test) >-{ >- if (shouldUseTracLinks()) >- return layoutTestsBasePath() + test; >- return testToURL(test, layoutTestsBasePath()); >-} >+ if (this.testResults.crashTests.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.crashTests, CrashingTestsSectionBuilder)); > >-function checkServerIsRunning(event) >-{ >- if (shouldUseTracLinks()) >- return; >- >- var 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"); >- }); >-} >+ if (this.testResults.crashOther.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.crashOther, OtherCrashesSectionBuilder)); > >-function testLink(test) >-{ >- return '<a class=test-link onclick="checkServerIsRunning(event)" href="' + layoutTestURL(test) + '">' + test + '</a><span class=flag onclick="unflag(this)"> \u2691</span>'; >-} >+ if (this.testResults.failingTests.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.failingTests, FailingTestsSectionBuilder)); > >-function unflag(flag) >-{ >- var shouldFlag = false; >- TestNavigator.flagTest(parentOfType(flag, 'tbody'), shouldFlag); >-} >+ if (this.testResults.missingResults.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.missingResults, TestsWithMissingResultsSectionBuilder)); > >-function testLinkWithExpandButton(test) >-{ >- return '<span class=expand-button onclick="toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + testLink(test); >-} >+ if (this.testResults.timeoutTests.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.timeoutTests, TimedOutTestsSectionBuilder)); > >-function testWithExpandButton(test) >-{ >- return '<span class=expand-button onclick="toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + test; >-} >+ if (this.testResults.testsWithStderr.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.testsWithStderr, TestsWithStdErrSectionBuilder)); > >-function resultLink(testPrefix, suffix, contents) >-{ >- return '<a class=result-link href="' + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> '; >-} >+ if (this.testResults.flakyPassTests.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.flakyPassTests, FlakyPassTestsSectionBuilder)); > >-function isFailureExpected(expected, actual) >-{ >- var isExpected = true; >- if (actual != 'SKIP') { >- var expectedArray = expected.split(' '); >- var actualArray = actual.split(' '); >- for (var i = 0; i < actualArray.length; i++) { >- var actualValue = actualArray[i]; >- if (expectedArray.indexOf(actualValue) == -1 && >- (expectedArray.indexOf('FAIL') == -1 || >- (actualValue != 'TEXT' && actualValue != 'IMAGE+TEXT' && actualValue != 'AUDIO'))) >- isExpected = false; >+ if (this.testResults.usesExpectationsFile() && this.testResults.unexpectedPassTests.length) >+ this.containerElement.appendChild(this.buildOneSection(this.testResults.unexpectedPassTests, UnexpectedPassTestsSectionBuilder)); >+ >+ 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(); > } >- return isExpected; >-} >+ >+ 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 = ''; >+ } > >-function processGlobalStateFor(testObject) >-{ >- var test = testObject.name; >- if (testObject.has_stderr) >- globalState().testsWithStderr.push(testObject); >+ 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'; >+ } > >- globalState().hasHttpTests = globalState().hasHttpTests || test.indexOf('http/') == 0; >+ buildOneSection(tests, sectionBuilderClass) >+ { >+ TestResults.sortByName(tests); >+ >+ let sectionBuilder = new sectionBuilderClass(tests, this); >+ return sectionBuilder.build(); >+ } > >- var actual = testObject.actual; >- var expected = testObject.expected || 'PASS'; >- if (globalState().results.uses_expectations_file) >- testObject.isExpected = isFailureExpected(expected, actual); >+ 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'); > >- if (actual == 'MISSING') { >- // FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for >- // tests with MISSING results. >- globalState().missingResults.push(testObject); >- return; >+ 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); >+ }) > } > >- var actualTokens = actual.split(' '); >- var passedWithImageOnlyFailureInRetry = actualTokens[0] == 'TEXT' && actualTokens[1] == 'IMAGE'; >- if (actualTokens[1] && actual.indexOf('PASS') != -1 || (!globalState().results.pixel_tests_enabled && passedWithImageOnlyFailureInRetry)) { >- globalState().flakyPassTests.push(testObject); >- return; >+ unflag(flag) >+ { >+ const shouldFlag = false; >+ testNavigator.flagTest(Utils.parentOfType(flag, 'tbody'), shouldFlag); > } > >- if (actual == 'PASS' && expected != 'PASS') { >- if (expected != 'IMAGE' || (globalState().results.pixel_tests_enabled || testObject.reftest_type)) { >- globalState().unexpectedPassTests.push(testObject); >- } >- return; >+ visibleTests(opt_container) >+ { >+ let container = opt_container || document; >+ if (this.onlyShowUnexpectedFailures()) >+ return container.querySelectorAll('tbody:not(.expected)'); >+ else >+ return container.querySelectorAll('tbody'); > } > >- if (actual == 'CRASH') { >- globalState().crashTests.push(testObject); >- return; >+ // FIXME: this is confusing. Flip the sense around. >+ onlyShowUnexpectedFailures() >+ { >+ return document.getElementById('unexpected-results').checked; > } > >- if (actual == 'TIMEOUT') { >- globalState().timeoutTests.push(testObject); >- return; >+ 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; > } >- >- globalState().failingTests.push(testObject); >-} > >-function toggleImages() >-{ >- var images = document.querySelectorAll('.animatedImage'); >- var imageTexts = document.querySelectorAll('.imageText'); >- for (var i = 0, len = images.length; i < len; i++) { >- var image = images[i]; >- var 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'; >+ 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; > } >-} > >-function textResultLinks(prefix) >-{ >- var html = resultLink(prefix, '-expected.txt', 'expected') + >- resultLink(prefix, '-actual.txt', 'actual') + >- resultLink(prefix, '-diff.txt', 'diff'); >+ layoutTestURL(testResult) >+ { >+ if (this.shouldUseTracLinks()) >+ return this.layoutTestsBasePath() + testResult.name; > >- if (globalState().results.has_pretty_patch) >- html += resultLink(prefix, '-pretty-diff.html', 'pretty diff'); >+ return this.testToURL(testResult, this.layoutTestsBasePath()); >+ } > >- if (globalState().results.has_wdiff) >- html += resultLink(prefix, '-wdiff.html', 'wdiff'); >+ 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 html; >-} >+ return basePath; >+ } > >-function imageResultsCell(testObject, testPrefix, actual) { >- var row = ''; >+ shouldUseTracLinks() >+ { >+ return !this.testResults.layoutTestsDir() || !location.toString().indexOf('file://') == 0; >+ } > >- if (actual.indexOf('IMAGE') != -1) { >- var testExtension = splitExtension(testObject.name)[1]; >- globalState().hasImageFailures = true; >+ checkServerIsRunning(event) >+ { >+ if (this.shouldUseTracLinks()) >+ return; > >- if (testObject.reftest_type && testObject.reftest_type.indexOf('!=') != -1) { >- row += resultLink(layoutTestsBasePath() + testPrefix, '-expected-mismatch.' + testExtension, 'ref mismatch'); >- row += resultLink(testPrefix, '-actual.png', 'actual'); >- } else { >- if (testObject.reftest_type && testObject.reftest_type.indexOf('==') != -1) { >- row += resultLink(layoutTestsBasePath() + testPrefix, '-expected.' + testExtension, 'reference'); >- } >- if (globalState().shouldToggleImages) { >- row += resultLink(testPrefix, '-diffs.html', 'images'); >- } else { >- row += resultLink(testPrefix, '-expected.png', 'expected'); >- row += resultLink(testPrefix, '-actual.png', 'actual'); >- } >+ let url = event.target.href; >+ if (url.startsWith("file://")) >+ return; > >- var diff = testObject.image_diff_percent; >- row += resultLink(testPrefix, '-diff.png', 'diff (' + diff + '%)'); >- } >+ 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>'; >+ } >+ >+ static resultLink(testPrefix, suffix, contents) >+ { >+ return '<a class=result-link href="' + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> '; > } > >- if (actual.indexOf('MISSING') != -1 && testObject.is_missing_image) >- row += resultLink(testPrefix, '-actual.png', 'png result'); >+ textResultLinks(prefix) >+ { >+ let html = TestResultsController.resultLink(prefix, '-expected.txt', 'expected') + >+ TestResultsController.resultLink(prefix, '-actual.txt', 'actual') + >+ TestResultsController.resultLink(prefix, '-diff.txt', 'diff'); > >- return row; >-} >+ if (this.testResults.hasPrettyPatch()) >+ html += TestResultsController.resultLink(prefix, '-pretty-diff.html', 'pretty diff'); > >-function flakinessDashboardURLForTests(testObjects) >-{ >- var testList = ""; >- for (var i = 0; i < testObjects.length; ++i) { >- testList += testObjects[i].name; >+ if (this.testResults.hasWDiff()) >+ html += TestResultsController.resultLink(prefix, '-wdiff.html', 'wdiff'); > >- if (i != testObjects.length - 1) >- testList += ","; >+ return html; > } > >- return 'http://webkit-test-results.webkit.org/dashboards/flakiness_dashboard.html#showAllRuns=true&tests=' + encodeURIComponent(testList); >-} >+ flakinessDashboardURLForTests(testObjects) >+ { >+ // FIXME: just map and join here. >+ let testList = ''; >+ for (let i = 0; i < testObjects.length; ++i) { >+ testList += testObjects[i].name; > >-function tableRow(testObject) >-{ >- var row = '<tbody' >- if (globalState().results.uses_expectations_file) >- row += ' class="' + (testObject.isExpected ? 'expected' : '') + '"'; >- if (testObject.reftest_type && testObject.reftest_type.indexOf('!=') != -1) >- row += ' mismatchreftest=true'; >- row += '><tr>'; >+ if (i != testObjects.length - 1) >+ testList += ','; >+ } > >- row += '<td>' + testLinkWithExpandButton(testObject.name) + '</td>'; >+ return 'http://webkit-test-results.webkit.org/dashboards/flakiness_dashboard.html#showAllRuns=true&tests=' + encodeURIComponent(testList); >+ } > >- var testPrefix = stripExtension(testObject.name); >- row += '<td>'; >- >- var actual = testObject.actual; >- if (actual.indexOf('TEXT') != -1) { >- globalState().hasTextFailures = true; >- row += textResultLinks(testPrefix); >+ _updatePageTitle() >+ { >+ let dateString = this.testResults.date(); >+ let title = document.createElement('title'); >+ title.textContent = 'Layout Test Results from ' + dateString; >+ document.head.appendChild(title); > } > >- if (actual.indexOf('AUDIO') != -1) { >- row += resultLink(testPrefix, '-expected.wav', 'expected audio'); >- row += resultLink(testPrefix, '-actual.wav', 'actual audio'); >- row += resultLink(testPrefix, '-diff.txt', 'textual diff'); >+ // Options handling. FIXME: move to a separate class? >+ updateAllOptions() >+ { >+ Utils.forEach(document.querySelectorAll('#options-menu input'), input => { input.onchange() }); > } > >- if (actual.indexOf('MISSING') != -1) { >- if (testObject.is_missing_audio) >- row += resultLink(testPrefix, '-actual.wav', 'audio result'); >- if (testObject.is_missing_text) >- row += resultLink(testPrefix, '-actual.txt', 'result'); >+ toggleOptionsMenu() >+ { >+ let menu = document.getElementById('options-menu'); >+ menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu'; > } > >- var actualTokens = actual.split(/\s+/); >- var cell = imageResultsCell(testObject, testPrefix, actualTokens[0]); >- if (!cell && actualTokens.length > 1) >- cell = imageResultsCell(testObject, 'retries/' + testPrefix, actualTokens[1]); >+ handleToggleUseNewlines() >+ { >+ OptionWriter.save(); >+ testNavigator.updateFlaggedTests(); >+ } >+ >+ handleUnexpectedResultsChange() >+ { >+ OptionWriter.save(); >+ this._updateExpectedFailures(); >+ } > >- row += '</td><td>' + cell + '</td>'; >+ expandAllExpectations() >+ { >+ let expandLinks = this._visibleExpandLinks(); >+ for (let link of expandLinks) >+ Utils.async(link => { controller.expandExpectations(link) }, [ link ]); >+ } > >- if (globalState().results.uses_expectations_file || actual.indexOf(' ') != -1) >- row += '<td>' + actual + '</td>'; >+ collapseAllExpectations() >+ { >+ let expandLinks = this._visibleExpandLinks(); >+ for (let link of expandLinks) >+ Utils.async(link => { controller.collapseExpectations(link) }, [ link ]); >+ } > >- if (globalState().results.uses_expectations_file) >- row += '<td>' + (actual.indexOf('MISSING') == -1 ? testObject.expected : '') + '</td>'; >+ 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 newRow = document.createElement('tr'); >+ newRow.className = 'results-row'; >+ let newCell = document.createElement('td'); >+ newCell.colSpan = row.querySelectorAll('td').length; >+ >+ 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); >+ } > >- row += '<td><a href="' + flakinessDashboardURLForTests([testObject]) + '">history</a></td>'; >+ newRow.appendChild(newCell); >+ parentTbody.appendChild(newRow); > >- row += '</tr></tbody>'; >- return row; >-} >+ this._updateExpandedState(newRow, true); > >-function forEachTest(handler, opt_tree, opt_prefix) >-{ >- var tree = opt_tree || globalState().results.tests; >- var prefix = opt_prefix || ''; >- >- for (var key in tree) { >- var newPrefix = prefix ? (prefix + '/' + key) : key; >- if ('actual' in tree[key]) { >- var testObject = tree[key]; >- testObject.name = newPrefix; >- handler(testObject); >- } else >- forEachTest(handler, tree[key], newPrefix); >+ this._updateImageTogglingTimer(); > } >-} > >-function forOtherCrashes() >-{ >- var tree = globalState().results.other_crashes; >- for (var key in tree) { >- var testObject = tree[key]; >- testObject.name = key; >- globalState().crashOther.push(testObject); >+ collapseExpectations(expandLink) >+ { >+ expandLink.textContent = '+'; >+ let existingResultsRow = Utils.parentOfType(expandLink, 'tbody').querySelector('.results-row'); >+ if (existingResultsRow) >+ this._updateExpandedState(existingResultsRow, false); > } >-} > >-function hasUnexpected(tests) >-{ >- return tests.some(function (test) { return !test.isExpected; }); >-} >+ toggleExpectations(element) >+ { >+ let expandLink = element; >+ if (expandLink.className != 'expand-button-text') >+ expandLink = expandLink.querySelector('.expand-button-text'); > >-function updateTestlistCounts() >-{ >- forEach(document.querySelectorAll('.test-list-count'), function(count) { >- var container = parentOfType(count, 'div'); >- var testContainers; >- if (onlyShowUnexpectedFailures()) >- testContainers = container.querySelectorAll('tbody:not(.expected)'); >+ if (expandLink.textContent == '+') >+ this.expandExpectations(expandLink); > else >- testContainers = container.querySelectorAll('tbody'); >- >- count.textContent = testContainers.length; >- }) >-} >+ this.collapseExpectations(expandLink); >+ } > >-function flagAll(headerLink) >-{ >- var tests = visibleTests(parentOfType(headerLink, 'div')); >- forEach(tests, function(tests) { >- var shouldFlag = true; >- TestNavigator.flagTest(tests, shouldFlag); >- }) >-} >+ _updateExpandedState(row, isExpanded) >+ { >+ row.setAttribute('data-expanded', isExpanded); >+ this._updateImageTogglingTimer(); >+ } > >-function testListHeaderHtml(header) >-{ >- return '<h1>' + header + ' (<span class=test-list-count></span>): <a href="#" class=flag-all onclick="flagAll(this)">flag all</a></h1>'; >-} >+ handleToggleImagesChange() >+ { >+ OptionWriter.save(); >+ this._updateTogglingImages(); >+ } > >-function testList(tests, header, tableId) >-{ >- tests.sort(function (a, b) { return a.name.localeCompare(b.name) }); >- >- var html = '<div' + ((!hasUnexpected(tests) && tableId != 'stderr-table') ? ' class=expected' : '') + ' id=' + tableId + '>' + >- testListHeaderHtml(header) + '<table>'; >- >- // FIXME: add the expected failure column for all the test lists if globalState().results.uses_expectations_file >- if (tableId == 'passes-table') >- html += '<thead><th>test</th><th>expected failure</th></thead>'; >- >- for (var i = 0; i < tests.length; i++) { >- var testObject = tests[i]; >- var test = testObject.name; >- html += '<tbody'; >- if (globalState().results.uses_expectations_file) >- html += ' class="' + ((testObject.isExpected && tableId != 'stderr-table') ? 'expected' : '') + '"'; >- html += '><tr><td>'; >- if (tableId == 'passes-table') >- html += testLink(test); >- else if (tableId == 'other-crash-tests-table') >- html += testWithExpandButton(test); >+ _visibleExpandLinks() >+ { >+ if (this.onlyShowUnexpectedFailures()) >+ return document.querySelectorAll('tbody:not(.expected) .expand-button-text'); > else >- html += testLinkWithExpandButton(test); >- >- html += '</td><td>'; >- >- if (tableId == 'stderr-table') >- html += resultLink(stripExtension(test), '-stderr.txt', 'stderr'); >- else if (tableId == 'passes-table') >- html += testObject.expected; >- else if (tableId == 'other-crash-tests-table') >- html += resultLink(stripExtension(test), '-crash-log.txt', 'crash log'); >- else if (tableId == 'crash-tests-table') { >- html += resultLink(stripExtension(test), '-crash-log.txt', 'crash log'); >- html += resultLink(stripExtension(test), '-sample.txt', 'sample'); >- } else if (tableId == 'timeout-tests-table') { >- // FIXME: only include timeout actual/diff results here if we actually spit out results for timeout tests. >- html += textResultLinks(stripExtension(test)); >- } >- >- if (tableId != 'other-crash-tests-table') >- html += '</td><td><a href="' + flakinessDashboardURLForTests([testObject]) + '">history</a></td>'; >- >- html += '</tr></tbody>'; >+ return document.querySelectorAll('.expand-button-text'); > } >- html += '</table></div>'; >- return html; >-} > >-function toArray(nodeList) >-{ >- return Array.prototype.slice.call(nodeList); >-} >+ static _togglingImage(prefix) >+ { >+ return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' + prefix + '"></img></div>'; >+ } > >-function trim(string) >-{ >- return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); >-} >+ _updateTogglingImages() >+ { >+ this.shouldToggleImages = document.getElementById('toggle-images').checked; > >-// Just a namespace for code management. >-var TableSorter = {}; >+ // 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); >+ })); >+ } > >-TableSorter._forwardArrow = '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>'; >+ this._updateImageTogglingTimer(); >+ } > >-TableSorter._backwardArrow = '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>'; >+ _updateExpectedFailures() >+ { >+ // Gross to do this by setting stylesheet text. Use a body class! >+ document.getElementById('unexpected-style').textContent = this.onlyShowUnexpectedFailures() ? '.expected { display: none; }' : ''; > >-TableSorter._sortedContents = function(header, arrow) >-{ >- return arrow + ' ' + trim(header.textContent) + ' ' + arrow; >-} >+ this.updateTestlistCounts(); >+ testNavigator.onlyShowUnexpectedFailuresChanged(); >+ } > >-TableSorter._updateHeaderClassNames = function(newHeader) >-{ >- var sortHeader = document.querySelector('.sortHeader'); >- if (sortHeader) { >- if (sortHeader == newHeader) { >- var isAlreadyReversed = sortHeader.classList.contains('reversed'); >- if (isAlreadyReversed) >- sortHeader.classList.remove('reversed'); >- else >- sortHeader.classList.add('reversed'); >+ 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 { >- sortHeader.textContent = sortHeader.textContent; >- sortHeader.classList.remove('sortHeader'); >- sortHeader.classList.remove('reversed'); >+ let lastDashIndex = src.lastIndexOf('-pretty'); >+ if (lastDashIndex == -1) >+ lastDashIndex = src.lastIndexOf('-'); >+ name = src.substring(lastDashIndex + 1); > } >- } > >- newHeader.classList.add('sortHeader'); >-} >+ let tagName = (src.lastIndexOf('.png') == -1) ? 'iframe' : 'img'; > >-TableSorter._textContent = function(tbodyRow, column) >-{ >- return tbodyRow.querySelectorAll('td')[column].textContent; >-} >+ if (tagName != 'img') >+ src += '?format=txt'; >+ return '<div class=result-container><div class=label>' + name + '</div><' + tagName + ' src="' + src + '"></' + tagName + '></div>'; >+ } > >-TableSorter._sortRows = function(newHeader, reversed) >-{ >- var testsTable = document.getElementById('results-table'); >- var headers = toArray(testsTable.querySelectorAll('th')); >- var sortColumn = headers.indexOf(newHeader); >- >- var rows = toArray(testsTable.querySelectorAll('tbody')); > >- rows.sort(function(a, b) { >- // Only need to support lexicographic sort for now. >- var aText = TableSorter._textContent(a, sortColumn); >- var bText = TableSorter._textContent(b, sortColumn); >- >- // Forward sort equal values by test name. >- if (sortColumn && aText == bText) { >- var aTestName = TableSorter._textContent(a, 0); >- var bTestName = TableSorter._textContent(b, 0); >- if (aTestName == bTestName) >- return 0; >- return aTestName < bTestName ? -1 : 1; >+ 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'; >+ } > } >+ } > >- if (reversed) >- return aText < bText ? 1 : -1; >- else >- return aText < bText ? -1 : 1; >- }); >- >- for (var i = 0; i < rows.length; i++) >- testsTable.appendChild(rows[i]); >-} >- >-TableSorter.sortColumn = function(columnNumber) >-{ >- var newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber]; >- TableSorter._sort(newHeader); >-} >- >-TableSorter.handleClick = function(e) >-{ >- var newHeader = e.target; >- if (newHeader.localName != 'th') >- return; >- TableSorter._sort(newHeader); >-} >+ _updateImageTogglingTimer() >+ { >+ let hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage'); >+ if (!hasVisibleAnimatedImage) { >+ clearInterval(this._togglingImageInterval); >+ this._togglingImageInterval = null; >+ return; >+ } > >-TableSorter._sort = function(newHeader) >-{ >- TableSorter._updateHeaderClassNames(newHeader); >+ if (!this._togglingImageInterval) { >+ TestResultsController._toggleImages(); >+ this._togglingImageInterval = setInterval(TestResultsController._toggleImages, 2000); >+ } >+ } > >- var reversed = newHeader.classList.contains('reversed'); >- var sortArrow = reversed ? TableSorter._backwardArrow : TableSorter._forwardArrow; >- newHeader.innerHTML = TableSorter._sortedContents(newHeader, sortArrow); >+ 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)); >+ } >+ } >+ } > >- TableSorter._sortRows(newHeader, reversed); >-} >+ 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'); >+ } >+ } >+}; > >-var PixelZoomer = {}; >+class SectionBuilder { >+ >+ constructor(tests, resultsController) >+ { >+ this._tests = tests; >+ this._table = null; >+ this._resultsController = resultsController; >+ } > >-PixelZoomer.showOnDelay = true; >-PixelZoomer._zoomFactor = 6; >+ 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; >+ } > >-var kResultWidth = 800; >-var kResultHeight = 600; >+ createTableRow(testResult) >+ { >+ let tbody = document.createElement('tbody'); >+ if (testResult.isExpected) >+ tbody.classList.add('expected'); >+ >+ let row = document.createElement('tr'); >+ tbody.appendChild(row); >+ >+ let testNameCell = document.createElement('td'); >+ this.fillTestCell(testResult, testNameCell); >+ row.appendChild(testNameCell); > >-var kZoomedResultWidth = kResultWidth * PixelZoomer._zoomFactor; >-var kZoomedResultHeight = kResultHeight * PixelZoomer._zoomFactor; >+ let resultCell = document.createElement('td'); >+ this.fillTestResultCell(testResult, resultCell); >+ row.appendChild(resultCell); > >-PixelZoomer._zoomImageContainer = function(url) >-{ >- var container = document.createElement('div'); >- container.className = 'zoom-image-container'; >+ let historyCell = this.createHistoryCell(testResult); >+ if (historyCell) >+ row.appendChild(historyCell); > >- var title = url.match(/\-([^\-]*)\.png/)[1]; >- >- var label = document.createElement('div'); >- label.className = 'label'; >- label.appendChild(document.createTextNode(title)); >- container.appendChild(label); >+ return tbody; >+ } > >- var imageContainer = document.createElement('div'); >- imageContainer.className = 'scaled-image-container'; >+ hideWhenShowingUnexpectedResultsOnly() >+ { >+ return !TestResults.hasUnexpectedResult(this._tests); >+ } > >- var image = new Image(); >- image.src = url; >- image.style.width = kZoomedResultWidth + 'px'; >- image.style.height = kZoomedResultHeight + 'px'; >- image.style.border = '1px solid black'; >- imageContainer.appendChild(image); >- container.appendChild(imageContainer); >+ addTableHeader() >+ { >+ } > >- return container; >-} >+ fillTestCell(testResult, cell) >+ { >+ cell.innerHTML = '<span class=expand-button onclick="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + this._resultsController.testLink(testResult); >+ } > >-PixelZoomer._createContainer = function(e) >-{ >- var tbody = parentOfType(e.target, 'tbody'); >- var row = tbody.querySelector('tr'); >- var imageDiffLinks = row.querySelectorAll('a[href$=".png"]'); >+ fillTestResultCell(testResult, cell) >+ { >+ } > >- var container = document.createElement('div'); >- container.className = 'pixel-zoom-container'; >+ createHistoryCell(testResult) >+ { >+ let historyCell = document.createElement('td'); >+ historyCell.innerHTML = '<a href="' + this._resultsController.flakinessDashboardURLForTests([testResult]) + '">history</a>' >+ return historyCell; >+ } > >- var html = ''; >+ tableID() { return ''; } >+ sectionTitle() { return ''; } >+}; >+ >+class FailuresSectionBuilder extends SectionBuilder { > >- var togglingImageLink = row.querySelector('a[href$="-diffs.html"]'); >- if (togglingImageLink) { >- var prefix = togglingImageLink.getAttribute('data-prefix'); >- container.appendChild(PixelZoomer._zoomImageContainer(prefix + '-expected.png')); >- container.appendChild(PixelZoomer._zoomImageContainer(prefix + '-actual.png')); >+ 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); > } > >- for (var i = 0; i < imageDiffLinks.length; i++) >- container.appendChild(PixelZoomer._zoomImageContainer(imageDiffLinks[i].href)); >+ createTableRow(testResult) >+ { >+ let tbody = document.createElement('tbody'); >+ if (testResult.isExpected) >+ tbody.classList.add('expected'); >+ >+ if (testResult.isMismatchRefTest()) >+ tbody.setAttribute('mismatchreftest', 'true'); > >- document.body.appendChild(container); >- PixelZoomer._drawAll(); >-} >+ let row = document.createElement('tr'); >+ 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.isMissing()) >+ 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); >+ } > >-PixelZoomer._draw = function(imageContainer) >-{ >- var image = imageContainer.querySelector('img'); >- var containerBounds = imageContainer.getBoundingClientRect(); >- image.style.left = (containerBounds.width / 2 - PixelZoomer._percentX * kZoomedResultWidth) + 'px'; >- image.style.top = (containerBounds.height / 2 - PixelZoomer._percentY * kZoomedResultHeight) + 'px'; >-} >+ if (this._resultsController.testResults.usesExpectationsFile()) { >+ let expectedCell = document.createElement('td'); >+ expectedCell.textContent = testResult.isMissing() ? '' : testResult.info.expected; >+ row.appendChild(expectedCell); >+ } > >-PixelZoomer._drawAll = function() >-{ >- forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), PixelZoomer._draw); >-} >+ let historyCell = this.createHistoryCell(testResult); >+ if (historyCell) >+ row.appendChild(historyCell); > >-PixelZoomer.handleMouseOut = function(e) >-{ >- if (e.relatedTarget && e.relatedTarget.tagName != 'IFRAME') >- return; >+ return tbody; >+ } > >- // If e.relatedTarget is null, we've moused out of the document. >- var container = document.querySelector('.pixel-zoom-container'); >- if (container) >- remove(container); >-} >+ 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) >+ { >+ if (testResult.info.is_missing_audio) >+ cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.wav', 'audio result'); > >-PixelZoomer.handleMouseMove = function(e) { >- if (PixelZoomer._mouseMoveTimeout) >- clearTimeout(PixelZoomer._mouseMoveTimeout); >+ if (testResult.info.is_missing_text) >+ cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.txt', 'result'); >+ } > >- if (parentOfType(e.target, '.pixel-zoom-container')) >- return; >+ imageResultLinks(testResult, testPrefix, resultToken) >+ { >+ let result = ''; >+ if (resultToken.indexOf('IMAGE') != -1) { >+ let testExtension = Utils.splitExtension(testResult.name)[1]; > >- var container = document.querySelector('.pixel-zoom-container'); >+ 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.isMissing() && testResult.isMissingImage()) >+ result += TestResultsController.resultLink(testPrefix, '-actual.png', 'png result'); >+ >+ return result; >+ } >+}; >+ >+class FailingTestsSectionBuilder extends FailuresSectionBuilder { >+ tableID() { return 'results-table'; } >+ sectionTitle() { return 'Tests that failed text/pixel/audio diff'; } >+}; >+ >+class TestsWithMissingResultsSectionBuilder extends FailuresSectionBuilder { >+ tableID() { return 'missing-table'; } >+ sectionTitle() { return 'Tests that had no expected results (probably new)'; } >+}; >+ >+class FlakyPassTestsSectionBuilder extends FailuresSectionBuilder { >+ tableID() { return 'flaky-tests-table'; } >+ sectionTitle() { return 'Flaky tests (failed the first run and passed on retry)'; } >+}; >+ >+class UnexpectedPassTestsSectionBuilder extends SectionBuilder { >+ tableID() { return 'passes-table'; } >+ 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); >+ } > >- var resultContainer = (e.target.className == 'result-container') ? >- e.target : parentOfType(e.target, '.result-container'); >- if (!resultContainer || !resultContainer.querySelector('img')) { >- if (container) >- remove(container); >- return; >+ fillTestCell(testResult, cell) >+ { >+ cell.innerHTML = '<a class=test-link onclick="controller.checkServerIsRunning(event)" href="' + this._resultsController.layoutTestURL(testResult) + '">' + testResult.name + '</a><span class=flag onclick="controller.unflag(this)"> \u2691</span>'; > } > >- var targetLocation = e.target.getBoundingClientRect(); >- PixelZoomer._percentX = (e.clientX - targetLocation.left) / targetLocation.width; >- PixelZoomer._percentY = (e.clientY - targetLocation.top) / targetLocation.height; >+ fillTestResultCell(testResult, cell) >+ { >+ cell.innerHTML = testResult.info.expected; >+ } >+}; > >- if (!container) { >- if (PixelZoomer.showOnDelay) { >- PixelZoomer._mouseMoveTimeout = setTimeout(function() { >- PixelZoomer._createContainer(e); >- }, 400); >- return; >- } > >- PixelZoomer._createContainer(e); >- return; >+class TestsWithStdErrSectionBuilder extends SectionBuilder { >+ tableID() { return 'stderr-table'; } >+ sectionTitle() { return 'Tests that had stderr output'; } >+ hideWhenShowingUnexpectedResultsOnly() { return false; } >+ >+ fillTestResultCell(testResult, cell) >+ { >+ cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-stderr.txt', 'stderr'); > } >- >- PixelZoomer._drawAll(); >-} >+}; > >-document.addEventListener('mousemove', PixelZoomer.handleMouseMove, false); >-document.addEventListener('mouseout', PixelZoomer.handleMouseOut, false); >+class TimedOutTestsSectionBuilder extends SectionBuilder { >+ tableID() { return 'timeout-tests-table'; } >+ sectionTitle() { return 'Tests that timed out'; } > >-var TestNavigator = {}; >+ 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)); >+ } >+}; > >-TestNavigator.reset = function() { >- TestNavigator.currentTestIndex = -1; >- TestNavigator.flaggedTests = {}; >-} >+class CrashingTestsSectionBuilder extends SectionBuilder { >+ tableID() { return 'crash-tests-table'; } >+ sectionTitle() { return 'Tests that crashed'; } > >-TestNavigator.handleKeyEvent = function(event) >-{ >- if (event.metaKey || event.shiftKey || event.ctrlKey) >- return; >- >- switch (String.fromCharCode(event.charCode)) { >- case 'i': >- TestNavigator._scrollToFirstTest(); >- break; >- case 'j': >- TestNavigator._scrollToNextTest(); >- break; >- case 'k': >- TestNavigator._scrollToPreviousTest(); >- break; >- case 'l': >- TestNavigator._scrollToLastTest(); >- break; >- case 'e': >- TestNavigator._expandCurrentTest(); >- break; >- case 'c': >- TestNavigator._collapseCurrentTest(); >- break; >- case 't': >- TestNavigator._toggleCurrentTest(); >- break; >- case 'f': >- TestNavigator._toggleCurrentTestFlagged(); >- break; >+ 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 { >+ tableID() { return 'other-crash-tests-table'; } >+ sectionTitle() { return 'Other crashes'; } >+ fillTestCell(testResult, cell) >+ { >+ cell.innerHTML = '<span class=expand-button onclick="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + testResult.name; > } >-} > >-TestNavigator._scrollToFirstTest = function() >-{ >- if (TestNavigator._setCurrentTest(0)) >- TestNavigator._scrollToCurrentTest(); >-} >+ fillTestResultCell(testResult, cell) >+ { >+ cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log'); >+ } > >-TestNavigator._scrollToLastTest = function() >-{ >- var links = visibleTests(); >- if (TestNavigator._setCurrentTest(links.length - 1)) >- TestNavigator._scrollToCurrentTest(); >-} >+ createHistoryCell(testResult) >+ { >+ return null; >+ } >+}; > >-TestNavigator._scrollToNextTest = function() >-{ >- if (TestNavigator.currentTestIndex == -1) >- TestNavigator._scrollToFirstTest(); >- else if (TestNavigator._setCurrentTest(TestNavigator.currentTestIndex + 1)) >- TestNavigator._scrollToCurrentTest(); >-} >+class PixelZoomer { >+ constructor() >+ { >+ this.showOnDelay = true; >+ this._zoomFactor = 6; > >-TestNavigator._scrollToPreviousTest = function() >-{ >- if (TestNavigator.currentTestIndex == -1) >- TestNavigator._scrollToLastTest(); >- else if (TestNavigator._setCurrentTest(TestNavigator.currentTestIndex - 1)) >- TestNavigator._scrollToCurrentTest(); >-} >+ this._resultWidth = 800; >+ this._resultHeight = 600; >+ >+ this._percentX = 0; >+ this._percentY = 0; > >-TestNavigator._currentTestLink = function() >-{ >- var links = visibleTests(); >- return links[TestNavigator.currentTestIndex]; >-} >+ document.addEventListener('mousemove', this, false); >+ document.addEventListener('mouseout', this, false); >+ } > >-TestNavigator._currentTestExpandLink = function() >-{ >- return TestNavigator._currentTestLink().querySelector('.expand-button-text'); >-} >+ _zoomedResultWidth() >+ { >+ return this._resultWidth * this._zoomFactor; >+ } >+ >+ _zoomedResultHeight() >+ { >+ return this._resultHeight * this._zoomFactor; >+ } >+ >+ _zoomImageContainer(url) >+ { >+ let container = document.createElement('div'); >+ container.className = 'zoom-image-container'; > >-TestNavigator._expandCurrentTest = function() >-{ >- expandExpectations(TestNavigator._currentTestExpandLink()); >-} >+ 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; >+ } > >-TestNavigator._collapseCurrentTest = function() >-{ >- collapseExpectations(TestNavigator._currentTestExpandLink()); >-} >+ _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)); > >-TestNavigator._toggleCurrentTest = function() >-{ >- toggleExpectations(TestNavigator._currentTestExpandLink()); >-} >+ document.body.appendChild(container); >+ this._drawAll(); >+ } > >-TestNavigator._toggleCurrentTestFlagged = function() >-{ >- var testLink = TestNavigator._currentTestLink(); >- TestNavigator.flagTest(testLink, !testLink.classList.contains('flagged')); >-} >+ _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'; >+ } > >-// FIXME: Test navigator shouldn't know anything about flagging. It should probably call out to TestFlagger or something. >-TestNavigator.flagTest = function(testTbody, shouldFlag) >-{ >- var testName = testTbody.querySelector('.test-link').innerText; >+ _drawAll() >+ { >+ Utils.forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), element => { this._draw(element) }); >+ } > >- if (shouldFlag) { >- testTbody.classList.add('flagged'); >- TestNavigator.flaggedTests[testName] = 1; >- } else { >- testTbody.classList.remove('flagged'); >- delete TestNavigator.flaggedTests[testName]; >+ handleEvent(event) >+ { >+ if (event.type == 'mousemove') { >+ this._handleMouseMove(event); >+ return; >+ } >+ >+ if (event.type == 'mouseout') { >+ this._handleMouseOut(event); >+ return; >+ } > } > >- TestNavigator.updateFlaggedTests(); >-} >+ _handleMouseOut(event) >+ { >+ if (event.relatedTarget && event.relatedTarget.tagName != 'IFRAME') >+ return; > >-TestNavigator.updateFlaggedTests = function() >-{ >- var flaggedTestTextbox = document.getElementById('flagged-tests'); >- if (!flaggedTestTextbox) { >- var 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'); >- } >- >- var flaggedTests = Object.keys(this.flaggedTests); >- flaggedTests.sort(); >- var separator = document.getElementById('use-newlines').checked ? '\n' : ' '; >- flaggedTestTextbox.innerHTML = flaggedTests.join(separator); >- document.getElementById('flagged-test-container').style.display = flaggedTests.length ? '' : 'none'; >-} >+ // If e.relatedTarget is null, we've moused out of the document. >+ let container = document.querySelector('.pixel-zoom-container'); >+ if (container) >+ container.remove(); >+ } > >-TestNavigator._setCurrentTest = function(testIndex) >-{ >- var links = visibleTests(); >- if (testIndex < 0 || testIndex >= links.length) >- return false; >+ _handleMouseMove(event) >+ { >+ if (this._mouseMoveTimeout) { >+ clearTimeout(this._mouseMoveTimeout); >+ this._mouseMoveTimeout = 0; >+ } > >- var currentTest = links[TestNavigator.currentTestIndex]; >- if (currentTest) >- currentTest.classList.remove('current'); >+ if (Utils.parentOfType(event.target, '.pixel-zoom-container')) >+ return; > >- TestNavigator.currentTestIndex = testIndex; >+ 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; >+ } > >- currentTest = links[TestNavigator.currentTestIndex]; >- currentTest.classList.add('current'); >+ let targetLocation = event.target.getBoundingClientRect(); >+ this._percentX = (event.clientX - targetLocation.left) / targetLocation.width; >+ this._percentY = (event.clientY - targetLocation.top) / targetLocation.height; > >- return true; >-} >+ if (!container) { >+ if (this.showOnDelay) { >+ this._mouseMoveTimeout = setTimeout(() => { >+ this._createContainer(event); >+ }, 400); >+ return; >+ } >+ >+ this._createContainer(event); >+ return; >+ } >+ >+ this._drawAll(); >+ } >+}; > >-TestNavigator._scrollToCurrentTest = function() >+class TableSorter > { >- var targetLink = TestNavigator._currentTestLink(); >- if (!targetLink) >- return; >+ static _forwardArrow() >+ { >+ return '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>'; >+ } > >- var 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.body.scrollTop += rowRect.top; >-} >+ static _backwardArrow() >+ { >+ return '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>'; >+ } > >-TestNavigator.onlyShowUnexpectedFailuresChanged = function() >-{ >- var currentTest = document.querySelector('.current'); >- if (!currentTest) >- return; >- >- // If our currentTest became hidden, reset the currentTestIndex. >- if (onlyShowUnexpectedFailures() && currentTest.classList.contains('expected')) >- TestNavigator._scrollToFirstTest(); >- else { >- // Recompute TestNavigator.currentTestIndex >- var links = visibleTests(); >- TestNavigator.currentTestIndex = links.indexOf(currentTest); >+ static _sortedContents(header, arrow) >+ { >+ return arrow + ' ' + Utils.trim(header.textContent) + ' ' + arrow; > } >-} > >-document.addEventListener('keypress', TestNavigator.handleKeyEvent, false); >+ 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'); >+ } > >-function onlyShowUnexpectedFailures() >-{ >- return document.getElementById('unexpected-results').checked; >-} >+ static _textContent(tbodyRow, column) >+ { >+ return tbodyRow.querySelectorAll('td')[column].textContent; >+ } > >-function handleUnexpectedResultsChange() >-{ >- OptionWriter.save(); >- updateExpectedFailures(); >-} >+ static _sortRows(newHeader, reversed) >+ { >+ let testsTable = document.getElementById('results-table'); >+ let headers = Utils.toArray(testsTable.querySelectorAll('th')); >+ let sortColumn = headers.indexOf(newHeader); > >-function updateExpectedFailures() >-{ >- document.getElementById('unexpected-style').textContent = onlyShowUnexpectedFailures() ? >- '.expected { display: none; }' : ''; >+ let rows = Utils.toArray(testsTable.querySelectorAll('tbody')); > >- updateTestlistCounts(); >- TestNavigator.onlyShowUnexpectedFailuresChanged(); >-} >+ 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; >+ } > >-var OptionWriter = {}; >+ if (reversed) >+ return aText < bText ? 1 : -1; >+ else >+ return aText < bText ? -1 : 1; >+ }); > >-OptionWriter._key = 'run-webkit-tests-options'; >+ for (let row of rows) >+ testsTable.appendChild(row); >+ } > >-OptionWriter.save = function() >-{ >- var options = document.querySelectorAll('label input'); >- var data = {}; >- for (var i = 0, len = options.length; i < len; i++) { >- var option = options[i]; >- data[option.id] = option.checked; >- } >- try { >- localStorage.setItem(OptionWriter._key, JSON.stringify(data)); >- } catch (err) { >- if (err.name != "SecurityError") >- throw err; >+ static sortColumn(columnNumber) >+ { >+ let newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber]; >+ TableSorter._sort(newHeader); > } >-} > >-OptionWriter.apply = function() >-{ >- var json; >- try { >- json = localStorage.getItem(OptionWriter._key); >- } catch (err) { >- if (err.name != "SecurityError") >- throw err; >+ static handleClick(e) >+ { >+ let newHeader = e.target; >+ if (newHeader.localName != 'th') >+ return; >+ TableSorter._sort(newHeader); > } > >- if (!json) { >- updateAllOptions(); >- return; >+ 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; >+ } > } > >- var data = JSON.parse(json); >- for (var id in data) { >- var input = document.getElementById(id); >- if (input) >- input.checked = data[id]; >+ 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(); > } >- updateAllOptions(); >-} > >-function updateAllOptions() >-{ >- forEach(document.querySelectorAll('#options-menu input'), function(input) { input.onchange(); }); >-} >+ static get _key() >+ { >+ return 'run-webkit-tests-options'; >+ } >+}; > >-function handleToggleUseNewlines() >+let testResults; >+function ADD_RESULTS(input) > { >- OptionWriter.save(); >- TestNavigator.updateFlaggedTests(); >+ testResults = new TestResults(input); > } >+</script> > >-function handleToggleImagesChange() >-{ >- OptionWriter.save(); >- updateTogglingImages(); >-} >+<script src="full_results.json"></script> >+ >+<script> > >-function updateTogglingImages() >+class TestNavigator > { >- var shouldToggle = document.getElementById('toggle-images').checked; >- globalState().shouldToggleImages = shouldToggle; >+ constructor() { >+ this.currentTestIndex = -1; >+ this.flaggedTests = {}; >+ document.addEventListener('keypress', this, false); >+ } > >- if (shouldToggle) { >- forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) a[href$=".png"]'), convertToTogglingHandler(function(prefix) { >- return resultLink(prefix, '-diffs.html', 'images'); >- })); >- forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) img[src$=".png"]'), convertToTogglingHandler(togglingImage)); >- } else { >- forEach(document.querySelectorAll('a[href$="-diffs.html"]'), convertToNonTogglingHandler(resultLink)); >- forEach(document.querySelectorAll('.animatedImage'), convertToNonTogglingHandler(function (absolutePrefix, suffix) { >- return resultIframe(absolutePrefix + suffix); >- })); >- } >- >- updateImageTogglingTimer(); >-} >+ handleEvent(event) >+ { >+ if (event.type == 'keypress') { >+ this.handleKeyEvent(event); >+ return; >+ } >+ } > >-function getResultContainer(node) >-{ >- return (node.tagName == 'IMG') ? parentOfType(node, '.result-container') : node; >-} >+ handleKeyEvent(event) >+ { >+ if (event.metaKey || event.shiftKey || event.ctrlKey) >+ return; > >-function convertToTogglingHandler(togglingImageFunction) >-{ >- return function(node) { >- var url = (node.tagName == 'IMG') ? node.src : node.href; >- if (url.match('-expected.png$')) >- remove(getResultContainer(node)); >- else if (url.match('-actual.png$')) { >- var name = parentOfType(node, 'tbody').querySelector('.test-link').textContent; >- getResultContainer(node).outerHTML = togglingImageFunction(stripExtension(name)); >+ 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; > } > } >-} > >-function convertToNonTogglingHandler(resultFunction) >-{ >- return function(node) { >- var prefix = node.getAttribute('data-prefix'); >- getResultContainer(node).outerHTML = resultFunction(prefix, '-expected.png', 'expected') + resultFunction(prefix, '-actual.png', 'actual'); >+ _scrollToFirstTest() >+ { >+ if (this._setCurrentTest(0)) >+ this._scrollToCurrentTest(); > } >-} > >-function toggleOptionsMenu() >-{ >- var menu = document.getElementById('options-menu'); >- menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu'; >-} >- >-function handleMouseDown(e) >-{ >- if (!parentOfType(e.target, '#options-menu') && e.target.id != 'options-link') >- document.getElementById('options-menu').className = 'hidden-menu'; >-} >- >-document.addEventListener('mousedown', handleMouseDown, false); >- >-function failingTestsTable(tests, title, id) >-{ >- if (!tests.length) >- return ''; >+ _scrollToLastTest() >+ { >+ let links = controller.visibleTests(); >+ if (this._setCurrentTest(links.length - 1)) >+ this._scrollToCurrentTest(); >+ } > >- var numberofUnexpectedFailures = 0; >- var tableRowHtml = ''; >- for (var i = 0; i < tests.length; i++){ >- tableRowHtml += tableRow(tests[i]); >- if (!tests[i].isExpected) >- numberofUnexpectedFailures++; >+ _scrollToNextTest() >+ { >+ if (this.currentTestIndex == -1) >+ this._scrollToFirstTest(); >+ else if (this._setCurrentTest(this.currentTestIndex + 1)) >+ this._scrollToCurrentTest(); > } > >- var header = '<div'; >- if (!hasUnexpected(tests)) >- header += ' class=expected'; >+ _scrollToPreviousTest() >+ { >+ if (this.currentTestIndex == -1) >+ this._scrollToLastTest(); >+ else if (this._setCurrentTest(this.currentTestIndex - 1)) >+ this._scrollToCurrentTest(); >+ } > >- header += '>' + testListHeaderHtml(title) + >- '<table id="' + id + '"><thead><tr>' + >- '<th>test</th>' + >- '<th id="text-results-header">results</th>' + >- '<th id="image-results-header">image results</th>'; >+ _currentTestLink() >+ { >+ let links = controller.visibleTests(); >+ return links[this.currentTestIndex]; >+ } > >- if (globalState().results.uses_expectations_file) >- header += '<th>actual failure</th><th>expected failure</th>'; >+ _currentTestExpandLink() >+ { >+ return this._currentTestLink().querySelector('.expand-button-text'); >+ } > >- header += '<th><a href="' + flakinessDashboardURLForTests(tests) + '">history</a></th>'; >+ _expandCurrentTest() >+ { >+ controller.expandExpectations(this._currentTestExpandLink()); >+ } > >- if (id == 'flaky-tests-table') >- header += '<th>failures</th>'; >+ _collapseCurrentTest() >+ { >+ controller.collapseExpectations(this._currentTestExpandLink()); >+ } > >- header += '</tr></thead>'; >+ _toggleCurrentTest() >+ { >+ controller.toggleExpectations(this._currentTestExpandLink()); >+ } > >- return header + tableRowHtml + '</table></div>'; >-} >+ _toggleCurrentTestFlagged() >+ { >+ let testLink = this._currentTestLink(); >+ this.flagTest(testLink, !testLink.classList.contains('flagged')); >+ } > >-function updateTitle() >-{ >- var dateString = globalState().results.date; >+ // 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; > >- var title = document.createElement('title'); >- title.textContent = 'Layout Test Results from ' + dateString; >- document.head.appendChild(title); >-} >- >-function generatePage() >-{ >- updateTitle(); >- forEachTest(processGlobalStateFor); >- forOtherCrashes(); >- >- var html = ""; >+ if (shouldFlag) { >+ testTbody.classList.add('flagged'); >+ this.flaggedTests[testName] = 1; >+ } else { >+ testTbody.classList.remove('flagged'); >+ delete this.flaggedTests[testName]; >+ } > >- if (globalState().results.interrupted) >- html += "<p class='stopped-running-early-message'>Testing exited early.</p>" >+ this.updateFlaggedTests(); >+ } > >- if (globalState().crashTests.length) >- html += testList(globalState().crashTests, 'Tests that crashed', 'crash-tests-table'); >+ 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'); >+ } > >- if (globalState().crashOther.length) >- html += testList(globalState().crashOther, 'Other Crashes', 'other-crash-tests-table'); >+ 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'; >+ } > >- html += failingTestsTable(globalState().failingTests, >- 'Tests that failed text/pixel/audio diff', 'results-table'); >+ _setCurrentTest(testIndex) >+ { >+ let links = controller.visibleTests(); >+ if (testIndex < 0 || testIndex >= links.length) >+ return false; > >- html += failingTestsTable(globalState().missingResults, >- 'Tests that had no expected results (probably new)', 'missing-table'); >+ let currentTest = links[this.currentTestIndex]; >+ if (currentTest) >+ currentTest.classList.remove('current'); > >- if (globalState().timeoutTests.length) >- html += testList(globalState().timeoutTests, 'Tests that timed out', 'timeout-tests-table'); >+ this.currentTestIndex = testIndex; > >- if (globalState().testsWithStderr.length) >- html += testList(globalState().testsWithStderr, 'Tests that had stderr output', 'stderr-table'); >+ currentTest = links[this.currentTestIndex]; >+ currentTest.classList.add('current'); > >- html += failingTestsTable(globalState().flakyPassTests, >- 'Flaky tests (failed the first run and passed on retry)', 'flaky-tests-table'); >+ return true; >+ } > >- if (globalState().results.uses_expectations_file && globalState().unexpectedPassTests.length) >- html += testList(globalState().unexpectedPassTests, 'Tests expected to fail but passed', 'passes-table'); >+ _scrollToCurrentTest() >+ { >+ let targetLink = this._currentTestLink(); >+ if (!targetLink) >+ return; > >- if (globalState().hasHttpTests) { >- html += '<p>httpd access log: <a href="access_log.txt">access_log.txt</a></p>' + >- '<p>httpd error log: <a href="error_log.txt">error_log.txt</a></p>'; >+ 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.body.scrollTop += rowRect.top; > } > >- document.getElementById('main-content').innerHTML = html + '</div>'; >+ onlyShowUnexpectedFailuresChanged() >+ { >+ let currentTest = document.querySelector('.current'); >+ if (!currentTest) >+ return; > >- if (document.getElementById('results-table')) { >- document.getElementById('results-table').addEventListener('click', TableSorter.handleClick, false); >- TableSorter.sortColumn(0); >- if (!globalState().results.uses_expectations_file) >- parentOfType(document.getElementById('unexpected-results'), 'label').style.display = 'none'; >- if (!globalState().hasTextFailures) >- document.getElementById('text-results-header').textContent = ''; >- if (!globalState().hasImageFailures) { >- document.getElementById('image-results-header').textContent = ''; >- parentOfType(document.getElementById('toggle-images'), 'label').style.display = 'none'; >+ // 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'; >+} > >- updateTestlistCounts(); >+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(); > >- TestNavigator.reset(); > OptionWriter.apply(); > } >+ >+window.addEventListener('load', generatePage, false); >+ > </script> >-<body onload="generatePage()"> >+<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 href="javascript:void()" onclick="expandAllExpectations()">expand all</a> >- <a href="javascript:void()" onclick="collapseAllExpectations()">collapse all</a> >- <a href="javascript:void()" id=options-link onclick="toggleOptionsMenu()">options</a> >+ <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="handleUnexpectedResultsChange()">Only unexpected results</label> >- <label><input id="toggle-images" type="checkbox" checked onchange="handleToggleImagesChange()">Toggle images</label> >- <label title="Use newlines instead of spaces to separate flagged tests"><input id="use-newlines" type="checkbox" checked onchange="handleToggleUseNewlines()">Use newlines in flagged list</input> >+ <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> >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Flags:
ap
:
review+
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 188690
: 347348