| /* |
| * Copyright (c) 2010 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| var ALL_DIRECTORY_PATH = '[all]'; |
| |
| var STATE_NEEDS_REBASELINE = 'needs_rebaseline'; |
| var STATE_REBASELINE_FAILED = 'rebaseline_failed'; |
| var STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'; |
| var STATE_IN_QUEUE = 'in_queue'; |
| var STATE_TO_DISPLAY_STATE = {}; |
| STATE_TO_DISPLAY_STATE[STATE_NEEDS_REBASELINE] = 'Needs rebaseline'; |
| STATE_TO_DISPLAY_STATE[STATE_REBASELINE_FAILED] = 'Rebaseline failed'; |
| STATE_TO_DISPLAY_STATE[STATE_REBASELINE_SUCCEEDED] = 'Rebaseline succeeded'; |
| STATE_TO_DISPLAY_STATE[STATE_IN_QUEUE] = 'In queue'; |
| |
| var results; |
| var testsByFailureType = {}; |
| var testsByDirectory = {}; |
| var selectedTests = []; |
| var loupe; |
| var queue; |
| |
| function main() |
| { |
| $('failure-type-selector').addEventListener('change', selectFailureType); |
| $('directory-selector').addEventListener('change', selectDirectory); |
| $('test-selector').addEventListener('change', selectTest); |
| $('next-test').addEventListener('click', nextTest); |
| $('previous-test').addEventListener('click', previousTest); |
| |
| $('toggle-log').addEventListener('click', function() { toggle('log'); }); |
| |
| loupe = new Loupe(); |
| queue = new RebaselineQueue(); |
| |
| document.addEventListener('keydown', function(event) { |
| if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { |
| return; |
| } |
| |
| switch (event.keyIdentifier) { |
| case 'Left': |
| event.preventDefault(); |
| previousTest(); |
| break; |
| case 'Right': |
| event.preventDefault(); |
| nextTest(); |
| break; |
| case 'U+0051': // q |
| queue.addCurrentTest(); |
| break; |
| case 'U+0058': // x |
| queue.removeCurrentTest(); |
| break; |
| case 'U+0052': // r |
| queue.rebaseline(); |
| break; |
| } |
| }); |
| |
| loadText('/platforms.json', function(text) { |
| var platforms = JSON.parse(text); |
| platforms.platforms.forEach(function(platform) { |
| var platformOption = document.createElement('option'); |
| platformOption.value = platform; |
| platformOption.textContent = platform; |
| |
| var targetOption = platformOption.cloneNode(true); |
| targetOption.selected = platform == platforms.defaultPlatform; |
| $('baseline-target').appendChild(targetOption); |
| $('baseline-move-to').appendChild(platformOption.cloneNode(true)); |
| }); |
| }); |
| |
| loadText('/results.json', function(text) { |
| results = JSON.parse(text); |
| displayResults(); |
| }); |
| } |
| |
| /** |
| * Groups test results by failure type. |
| */ |
| function displayResults() |
| { |
| var failureTypeSelector = $('failure-type-selector'); |
| var failureTypes = []; |
| |
| for (var testName in results.tests) { |
| var test = results.tests[testName]; |
| if (test.actual == 'PASS') { |
| continue; |
| } |
| var failureType = test.actual + ' (expected ' + test.expected + ')'; |
| if (!(failureType in testsByFailureType)) { |
| testsByFailureType[failureType] = []; |
| failureTypes.push(failureType); |
| } |
| testsByFailureType[failureType].push(testName); |
| } |
| |
| // Sort by number of failures |
| failureTypes.sort(function(a, b) { |
| return testsByFailureType[b].length - testsByFailureType[a].length; |
| }); |
| |
| for (var i = 0, failureType; failureType = failureTypes[i]; i++) { |
| var failureTypeOption = document.createElement('option'); |
| failureTypeOption.value = failureType; |
| failureTypeOption.textContent = failureType + ' - ' + testsByFailureType[failureType].length + ' tests'; |
| failureTypeSelector.appendChild(failureTypeOption); |
| } |
| |
| selectFailureType(); |
| |
| document.body.className = ''; |
| } |
| |
| /** |
| * For a given failure type, gets all the tests and groups them by directory |
| * (populating the directory selector with them). |
| */ |
| function selectFailureType() |
| { |
| var selectedFailureType = getSelectValue('failure-type-selector'); |
| var tests = testsByFailureType[selectedFailureType]; |
| |
| testsByDirectory = {} |
| var displayDirectoryNamesByDirectory = {}; |
| var directories = []; |
| |
| // Include a special option for all tests |
| testsByDirectory[ALL_DIRECTORY_PATH] = tests; |
| displayDirectoryNamesByDirectory[ALL_DIRECTORY_PATH] = 'all'; |
| directories.push(ALL_DIRECTORY_PATH); |
| |
| // Roll up tests by ancestor directories |
| tests.forEach(function(test) { |
| var pathPieces = test.split('/'); |
| var pathDirectories = pathPieces.slice(0, pathPieces.length -1); |
| var ancestorDirectory = ''; |
| |
| pathDirectories.forEach(function(pathDirectory, index) { |
| ancestorDirectory += pathDirectory + '/'; |
| if (!(ancestorDirectory in testsByDirectory)) { |
| testsByDirectory[ancestorDirectory] = []; |
| var displayDirectoryName = new Array(index * 6).join(' ') + pathDirectory; |
| displayDirectoryNamesByDirectory[ancestorDirectory] = displayDirectoryName; |
| directories.push(ancestorDirectory); |
| } |
| |
| testsByDirectory[ancestorDirectory].push(test); |
| }); |
| }); |
| |
| directories.sort(); |
| |
| var directorySelector = $('directory-selector'); |
| directorySelector.innerHTML = ''; |
| |
| directories.forEach(function(directory) { |
| var directoryOption = document.createElement('option'); |
| directoryOption.value = directory; |
| directoryOption.innerHTML = |
| displayDirectoryNamesByDirectory[directory] + ' - ' + |
| testsByDirectory[directory].length + ' tests'; |
| directorySelector.appendChild(directoryOption); |
| }); |
| |
| selectDirectory(); |
| } |
| |
| /** |
| * For a given failure type and directory and failure type, gets all the tests |
| * in that directory and populatest the test selector with them. |
| */ |
| function selectDirectory() |
| { |
| var previouslySelectedTest = getSelectedTest(); |
| |
| var selectedDirectory = getSelectValue('directory-selector'); |
| selectedTests = testsByDirectory[selectedDirectory]; |
| selectedTests.sort(); |
| |
| var testsByState = {}; |
| selectedTests.forEach(function(testName) { |
| var state = results.tests[testName].state; |
| if (state == STATE_IN_QUEUE) { |
| state = STATE_NEEDS_REBASELINE; |
| } |
| if (!(state in testsByState)) { |
| testsByState[state] = []; |
| } |
| testsByState[state].push(testName); |
| }); |
| |
| var optionIndexByTest = {}; |
| |
| var testSelector = $('test-selector'); |
| testSelector.innerHTML = ''; |
| |
| for (var state in testsByState) { |
| var stateOption = document.createElement('option'); |
| stateOption.textContent = STATE_TO_DISPLAY_STATE[state]; |
| stateOption.disabled = true; |
| testSelector.appendChild(stateOption); |
| |
| testsByState[state].forEach(function(testName) { |
| var testOption = document.createElement('option'); |
| testOption.value = testName; |
| var testDisplayName = testName; |
| if (testName.lastIndexOf(selectedDirectory) == 0) { |
| testDisplayName = testName.substring(selectedDirectory.length); |
| } |
| testOption.innerHTML = ' ' + testDisplayName; |
| optionIndexByTest[testName] = testSelector.options.length; |
| testSelector.appendChild(testOption); |
| }); |
| } |
| |
| if (previouslySelectedTest in optionIndexByTest) { |
| testSelector.selectedIndex = optionIndexByTest[previouslySelectedTest]; |
| } else if (STATE_NEEDS_REBASELINE in testsByState) { |
| testSelector.selectedIndex = |
| optionIndexByTest[testsByState[STATE_NEEDS_REBASELINE][0]]; |
| selectTest(); |
| } else { |
| testSelector.selectedIndex = 1; |
| selectTest(); |
| } |
| |
| selectTest(); |
| } |
| |
| function getSelectedTest() |
| { |
| return getSelectValue('test-selector'); |
| } |
| |
| function selectTest() |
| { |
| var selectedTest = getSelectedTest(); |
| |
| if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) { |
| $('image-outputs').style.display = ''; |
| displayImageResults(selectedTest); |
| } else { |
| $('image-outputs').style.display = 'none'; |
| } |
| |
| if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) { |
| $('text-outputs').style.display = ''; |
| displayTextResults(selectedTest); |
| } else { |
| $('text-outputs').style.display = 'none'; |
| } |
| |
| var currentBaselines = $('current-baselines'); |
| currentBaselines.textContent = ''; |
| var baselines = results.tests[selectedTest].baselines; |
| var testName = selectedTest.split('.').slice(0, -1).join('.'); |
| getSortedKeys(baselines).forEach(function(platform, i) { |
| if (i != 0) { |
| currentBaselines.appendChild(document.createTextNode('; ')); |
| } |
| var platformName = document.createElement('span'); |
| platformName.className = 'platform'; |
| platformName.textContent = platform; |
| currentBaselines.appendChild(platformName); |
| currentBaselines.appendChild(document.createTextNode(' (')); |
| getSortedKeys(baselines[platform]).forEach(function(extension, j) { |
| if (j != 0) { |
| currentBaselines.appendChild(document.createTextNode(', ')); |
| } |
| var link = document.createElement('a'); |
| var baselinePath = ''; |
| if (platform != 'base') { |
| baselinePath += 'platform/' + platform + '/'; |
| } |
| baselinePath += testName + '-expected' + extension; |
| link.href = getTracUrl(baselinePath); |
| if (extension == '.checksum') { |
| link.textContent = 'chk'; |
| } else { |
| link.textContent = extension.substring(1); |
| } |
| link.target = '_blank'; |
| if (baselines[platform][extension]) { |
| link.className = 'was-used-for-test'; |
| } |
| currentBaselines.appendChild(link); |
| }); |
| currentBaselines.appendChild(document.createTextNode(')')); |
| }); |
| |
| updateState(); |
| loupe.hide(); |
| |
| prefetchNextImageTest(); |
| } |
| |
| function prefetchNextImageTest() |
| { |
| var testSelector = $('test-selector'); |
| if (testSelector.selectedIndex == testSelector.options.length - 1) { |
| return; |
| } |
| var nextTest = testSelector.options[testSelector.selectedIndex + 1].value; |
| if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) { |
| new Image().src = getTestResultUrl(nextTest, 'expected-image'); |
| new Image().src = getTestResultUrl(nextTest, 'actual-image'); |
| } |
| } |
| |
| function updateState() |
| { |
| var testName = getSelectedTest(); |
| var testIndex = selectedTests.indexOf(testName); |
| var testCount = selectedTests.length |
| $('test-index').textContent = testIndex + 1; |
| $('test-count').textContent = testCount; |
| |
| $('next-test').disabled = testIndex == testCount - 1; |
| $('previous-test').disabled = testIndex == 0; |
| |
| $('test-link').href = getTracUrl(testName); |
| |
| var state = results.tests[testName].state; |
| $('state').className = state; |
| $('state').innerHTML = STATE_TO_DISPLAY_STATE[state]; |
| |
| queue.updateState(); |
| } |
| |
| function getTestResultUrl(testName, mode) |
| { |
| return '/test_result?test=' + testName + '&mode=' + mode; |
| } |
| |
| var currentExpectedImageTest; |
| var currentActualImageTest; |
| |
| function displayImageResults(testName) |
| { |
| if (currentExpectedImageTest == currentActualImageTest |
| && currentExpectedImageTest == testName) { |
| return; |
| } |
| |
| function displayImageResult(mode, callback) { |
| var image = $(mode); |
| image.className = 'loading'; |
| image.src = getTestResultUrl(testName, mode); |
| image.onload = function() { |
| image.className = ''; |
| callback(); |
| updateImageDiff(); |
| }; |
| } |
| |
| displayImageResult( |
| 'expected-image', |
| function() { currentExpectedImageTest = testName; }); |
| displayImageResult( |
| 'actual-image', |
| function() { currentActualImageTest = testName; }); |
| |
| $('diff-canvas').className = 'loading'; |
| $('diff-canvas').style.display = ''; |
| $('diff-checksum').style.display = 'none'; |
| } |
| |
| /** |
| * Computes a graphical a diff between the expected and actual images by |
| * rendering each to a canvas, getting the image data, and comparing the RGBA |
| * components of each pixel. The output is put into the diff canvas, with |
| * identical pixels appearing at 12.5% opacity and different pixels being |
| * highlighted in red. |
| */ |
| function updateImageDiff() { |
| if (currentExpectedImageTest != currentActualImageTest) |
| return; |
| |
| var expectedImage = $('expected-image'); |
| var actualImage = $('actual-image'); |
| |
| function getImageData(image) { |
| var imageCanvas = document.createElement('canvas'); |
| imageCanvas.width = image.width; |
| imageCanvas.height = image.height; |
| imageCanvasContext = imageCanvas.getContext('2d'); |
| |
| imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)'; |
| imageCanvasContext.fillRect( |
| 0, 0, image.width, image.height); |
| |
| imageCanvasContext.drawImage(image, 0, 0); |
| return imageCanvasContext.getImageData( |
| 0, 0, image.width, image.height); |
| } |
| |
| var expectedImageData = getImageData(expectedImage); |
| var actualImageData = getImageData(actualImage); |
| |
| var diffCanvas = $('diff-canvas'); |
| var diffCanvasContext = diffCanvas.getContext('2d'); |
| var diffImageData = |
| diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height); |
| |
| // Avoiding property lookups for all these during the per-pixel loop below |
| // provides a significant performance benefit. |
| var expectedWidth = expectedImage.width; |
| var expectedHeight = expectedImage.height; |
| var expected = expectedImageData.data; |
| |
| var actualWidth = actualImage.width; |
| var actual = actualImageData.data; |
| |
| var diffWidth = diffImageData.width; |
| var diff = diffImageData.data; |
| |
| var hadDiff = false; |
| for (var x = 0; x < expectedWidth; x++) { |
| for (var y = 0; y < expectedHeight; y++) { |
| var expectedOffset = (y * expectedWidth + x) * 4; |
| var actualOffset = (y * actualWidth + x) * 4; |
| var diffOffset = (y * diffWidth + x) * 4; |
| if (expected[expectedOffset] != actual[actualOffset] || |
| expected[expectedOffset + 1] != actual[actualOffset + 1] || |
| expected[expectedOffset + 2] != actual[actualOffset + 2] || |
| expected[expectedOffset + 3] != actual[actualOffset + 3]) { |
| hadDiff = true; |
| diff[diffOffset] = 255; |
| diff[diffOffset + 1] = 0; |
| diff[diffOffset + 2] = 0; |
| diff[diffOffset + 3] = 255; |
| } else { |
| diff[diffOffset] = expected[expectedOffset]; |
| diff[diffOffset + 1] = expected[expectedOffset + 1]; |
| diff[diffOffset + 2] = expected[expectedOffset + 2]; |
| diff[diffOffset + 3] = 32; |
| } |
| } |
| } |
| |
| diffCanvasContext.putImageData( |
| diffImageData, |
| 0, 0, |
| 0, 0, |
| diffImageData.width, diffImageData.height); |
| diffCanvas.className = ''; |
| |
| if (!hadDiff) { |
| diffCanvas.style.display = 'none'; |
| $('diff-checksum').style.display = ''; |
| loadTextResult(currentExpectedImageTest, 'expected-checksum'); |
| loadTextResult(currentExpectedImageTest, 'actual-checksum'); |
| } |
| } |
| |
| function loadTextResult(testName, mode, responseIsHtml) |
| { |
| loadText(getTestResultUrl(testName, mode), function(text) { |
| if (responseIsHtml) { |
| $(mode).innerHTML = text; |
| } else { |
| $(mode).textContent = text; |
| } |
| }); |
| } |
| |
| function displayTextResults(testName) |
| { |
| loadTextResult(testName, 'expected-text'); |
| loadTextResult(testName, 'actual-text'); |
| loadTextResult(testName, 'diff-text-pretty', true); |
| } |
| |
| function nextTest() |
| { |
| var testSelector = $('test-selector'); |
| var nextTestIndex = testSelector.selectedIndex + 1; |
| while (true) { |
| if (nextTestIndex == testSelector.options.length) { |
| return; |
| } |
| if (testSelector.options[nextTestIndex].disabled) { |
| nextTestIndex++; |
| } else { |
| testSelector.selectedIndex = nextTestIndex; |
| selectTest(); |
| return; |
| } |
| } |
| } |
| |
| function previousTest() |
| { |
| var testSelector = $('test-selector'); |
| var previousTestIndex = testSelector.selectedIndex - 1; |
| while (true) { |
| if (previousTestIndex == -1) { |
| return; |
| } |
| if (testSelector.options[previousTestIndex].disabled) { |
| previousTestIndex--; |
| } else { |
| testSelector.selectedIndex = previousTestIndex; |
| selectTest(); |
| return |
| } |
| } |
| } |
| |
| window.addEventListener('DOMContentLoaded', main); |