| <!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> |
| if (window.testRunner) |
| testRunner.dumpAsText(); |
| |
| 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'); |
| } |
| |
| const diff = Math.max(testResult.info.image_diff_percent, 0.01).toFixed(2); |
| 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(); |
| } |
| |
| 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> |