2 * Copyright (c) 2010 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 var ALL_DIRECTORY_PATH = '[all]';
33 var STATE_NEEDS_REBASELINE = 'needs_rebaseline';
34 var STATE_REBASELINE_FAILED = 'rebaseline_failed';
35 var STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded';
36 var STATE_IN_QUEUE = 'in_queue';
37 var STATE_TO_DISPLAY_STATE = {};
38 STATE_TO_DISPLAY_STATE[STATE_NEEDS_REBASELINE] = 'Needs rebaseline';
39 STATE_TO_DISPLAY_STATE[STATE_REBASELINE_FAILED] = 'Rebaseline failed';
40 STATE_TO_DISPLAY_STATE[STATE_REBASELINE_SUCCEEDED] = 'Rebaseline succeeded';
41 STATE_TO_DISPLAY_STATE[STATE_IN_QUEUE] = 'In queue';
44 var testsByFailureType = {};
45 var testsByDirectory = {};
46 var selectedTests = [];
52 $('failure-type-selector').addEventListener('change', selectFailureType);
53 $('directory-selector').addEventListener('change', selectDirectory);
54 $('test-selector').addEventListener('change', selectTest);
55 $('next-test').addEventListener('click', nextTest);
56 $('previous-test').addEventListener('click', previousTest);
58 $('toggle-log').addEventListener('click', function() { toggle('log'); });
61 queue = new RebaselineQueue();
63 document.addEventListener('keydown', function(event) {
64 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
68 switch (event.keyIdentifier) {
70 event.preventDefault();
74 event.preventDefault();
78 queue.addCurrentTest();
81 queue.removeCurrentTest();
89 loadText('/platforms.json', function(text) {
90 var platforms = JSON.parse(text);
91 platforms.platforms.forEach(function(platform) {
92 var platformOption = document.createElement('option');
93 platformOption.value = platform;
94 platformOption.textContent = platform;
96 var targetOption = platformOption.cloneNode(true);
97 targetOption.selected = platform == platforms.defaultPlatform;
98 $('baseline-target').appendChild(targetOption);
99 $('baseline-move-to').appendChild(platformOption.cloneNode(true));
103 loadText('/results.json', function(text) {
104 results = JSON.parse(text);
110 * Groups test results by failure type.
112 function displayResults()
114 var failureTypeSelector = $('failure-type-selector');
115 var failureTypes = [];
117 for (var testName in results.tests) {
118 var test = results.tests[testName];
119 if (test.actual == 'PASS') {
122 var failureType = test.actual + ' (expected ' + test.expected + ')';
123 if (!(failureType in testsByFailureType)) {
124 testsByFailureType[failureType] = [];
125 failureTypes.push(failureType);
127 testsByFailureType[failureType].push(testName);
130 // Sort by number of failures
131 failureTypes.sort(function(a, b) {
132 return testsByFailureType[b].length - testsByFailureType[a].length;
135 for (var i = 0, failureType; failureType = failureTypes[i]; i++) {
136 var failureTypeOption = document.createElement('option');
137 failureTypeOption.value = failureType;
138 failureTypeOption.textContent = failureType + ' - ' + testsByFailureType[failureType].length + ' tests';
139 failureTypeSelector.appendChild(failureTypeOption);
144 document.body.className = '';
148 * For a given failure type, gets all the tests and groups them by directory
149 * (populating the directory selector with them).
151 function selectFailureType()
153 var selectedFailureType = getSelectValue('failure-type-selector');
154 var tests = testsByFailureType[selectedFailureType];
156 testsByDirectory = {}
157 var displayDirectoryNamesByDirectory = {};
158 var directories = [];
160 // Include a special option for all tests
161 testsByDirectory[ALL_DIRECTORY_PATH] = tests;
162 displayDirectoryNamesByDirectory[ALL_DIRECTORY_PATH] = 'all';
163 directories.push(ALL_DIRECTORY_PATH);
165 // Roll up tests by ancestor directories
166 tests.forEach(function(test) {
167 var pathPieces = test.split('/');
168 var pathDirectories = pathPieces.slice(0, pathPieces.length -1);
169 var ancestorDirectory = '';
171 pathDirectories.forEach(function(pathDirectory, index) {
172 ancestorDirectory += pathDirectory + '/';
173 if (!(ancestorDirectory in testsByDirectory)) {
174 testsByDirectory[ancestorDirectory] = [];
175 var displayDirectoryName = new Array(index * 6).join(' ') + pathDirectory;
176 displayDirectoryNamesByDirectory[ancestorDirectory] = displayDirectoryName;
177 directories.push(ancestorDirectory);
180 testsByDirectory[ancestorDirectory].push(test);
186 var directorySelector = $('directory-selector');
187 directorySelector.innerHTML = '';
189 directories.forEach(function(directory) {
190 var directoryOption = document.createElement('option');
191 directoryOption.value = directory;
192 directoryOption.innerHTML =
193 displayDirectoryNamesByDirectory[directory] + ' - ' +
194 testsByDirectory[directory].length + ' tests';
195 directorySelector.appendChild(directoryOption);
202 * For a given failure type and directory and failure type, gets all the tests
203 * in that directory and populatest the test selector with them.
205 function selectDirectory()
207 var selectedDirectory = getSelectValue('directory-selector');
208 selectedTests = testsByDirectory[selectedDirectory];
210 selectedTests.sort();
212 var testSelector = $('test-selector');
213 testSelector.innerHTML = '';
215 selectedTests.forEach(function(testName) {
216 var testOption = document.createElement('option');
217 testOption.value = testName;
218 var testDisplayName = testName;
219 if (testName.lastIndexOf(selectedDirectory) == 0) {
220 testDisplayName = testName.substring(selectedDirectory.length);
222 testOption.innerHTML = ' ' + testDisplayName;
223 testSelector.appendChild(testOption);
229 function getSelectedTest()
231 return getSelectValue('test-selector');
234 function selectTest()
236 var selectedTest = getSelectedTest();
238 if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) {
239 $('image-outputs').style.display = '';
240 displayImageResults(selectedTest);
242 $('image-outputs').style.display = 'none';
245 if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) {
246 $('text-outputs').style.display = '';
247 displayTextResults(selectedTest);
249 $('text-outputs').style.display = 'none';
252 var currentBaselines = $('current-baselines');
253 currentBaselines.textContent = '';
254 var baselines = results.tests[selectedTest].baselines;
255 var testName = selectedTest.split('.').slice(0, -1).join('.');
256 getSortedKeys(baselines).forEach(function(platform, i) {
258 currentBaselines.appendChild(document.createTextNode('; '));
260 var platformName = document.createElement('span');
261 platformName.className = 'platform';
262 platformName.textContent = platform;
263 currentBaselines.appendChild(platformName);
264 currentBaselines.appendChild(document.createTextNode(' ('));
265 getSortedKeys(baselines[platform]).forEach(function(extension, j) {
267 currentBaselines.appendChild(document.createTextNode(', '));
269 var link = document.createElement('a');
270 var baselinePath = '';
271 if (platform != 'base') {
272 baselinePath += 'platform/' + platform + '/';
274 baselinePath += testName + '-expected' + extension;
275 link.href = getTracUrl(baselinePath);
276 if (extension == '.checksum') {
277 link.textContent = 'chk';
279 link.textContent = extension.substring(1);
281 link.target = '_blank';
282 if (baselines[platform][extension]) {
283 link.className = 'was-used-for-test';
285 currentBaselines.appendChild(link);
287 currentBaselines.appendChild(document.createTextNode(')'));
293 prefetchNextImageTest();
296 function prefetchNextImageTest()
298 var testSelector = $('test-selector');
299 if (testSelector.selectedIndex == testSelector.options.length - 1) {
302 var nextTest = testSelector.options[testSelector.selectedIndex + 1].value;
303 if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) {
304 new Image().src = getTestResultUrl(nextTest, 'expected-image');
305 new Image().src = getTestResultUrl(nextTest, 'actual-image');
309 function updateState()
311 var testName = getSelectedTest();
312 var testIndex = selectedTests.indexOf(testName);
313 var testCount = selectedTests.length
314 $('test-index').textContent = testIndex + 1;
315 $('test-count').textContent = testCount;
317 $('next-test').disabled = testIndex == testCount - 1;
318 $('previous-test').disabled = testIndex == 0;
320 $('test-link').href = getTracUrl(testName);
322 var state = results.tests[testName].state;
323 $('state').className = state;
324 $('state').innerHTML = STATE_TO_DISPLAY_STATE[state];
329 function getTestResultUrl(testName, mode)
331 return '/test_result?test=' + testName + '&mode=' + mode;
334 var currentExpectedImageTest;
335 var currentActualImageTest;
337 function displayImageResults(testName)
339 if (currentExpectedImageTest == currentActualImageTest
340 && currentExpectedImageTest == testName) {
344 function displayImageResult(mode, callback) {
346 image.className = 'loading';
347 image.src = getTestResultUrl(testName, mode);
348 image.onload = function() {
349 image.className = '';
357 function() { currentExpectedImageTest = testName; });
360 function() { currentActualImageTest = testName; });
362 $('diff-canvas').className = 'loading';
363 $('diff-canvas').style.display = '';
364 $('diff-checksum').style.display = 'none';
368 * Computes a graphical a diff between the expected and actual images by
369 * rendering each to a canvas, getting the image data, and comparing the RGBA
370 * components of each pixel. The output is put into the diff canvas, with
371 * identical pixels appearing at 12.5% opacity and different pixels being
372 * highlighted in red.
374 function updateImageDiff() {
375 if (currentExpectedImageTest != currentActualImageTest)
378 var expectedImage = $('expected-image');
379 var actualImage = $('actual-image');
381 function getImageData(image) {
382 var imageCanvas = document.createElement('canvas');
383 imageCanvas.width = image.width;
384 imageCanvas.height = image.height;
385 imageCanvasContext = imageCanvas.getContext('2d');
387 imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)';
388 imageCanvasContext.fillRect(
389 0, 0, image.width, image.height);
391 imageCanvasContext.drawImage(image, 0, 0);
392 return imageCanvasContext.getImageData(
393 0, 0, image.width, image.height);
396 var expectedImageData = getImageData(expectedImage);
397 var actualImageData = getImageData(actualImage);
399 var diffCanvas = $('diff-canvas');
400 var diffCanvasContext = diffCanvas.getContext('2d');
402 diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height);
404 // Avoiding property lookups for all these during the per-pixel loop below
405 // provides a significant performance benefit.
406 var expectedWidth = expectedImage.width;
407 var expectedHeight = expectedImage.height;
408 var expected = expectedImageData.data;
410 var actualWidth = actualImage.width;
411 var actual = actualImageData.data;
413 var diffWidth = diffImageData.width;
414 var diff = diffImageData.data;
417 for (var x = 0; x < expectedWidth; x++) {
418 for (var y = 0; y < expectedHeight; y++) {
419 var expectedOffset = (y * expectedWidth + x) * 4;
420 var actualOffset = (y * actualWidth + x) * 4;
421 var diffOffset = (y * diffWidth + x) * 4;
422 if (expected[expectedOffset] != actual[actualOffset] ||
423 expected[expectedOffset + 1] != actual[actualOffset + 1] ||
424 expected[expectedOffset + 2] != actual[actualOffset + 2] ||
425 expected[expectedOffset + 3] != actual[actualOffset + 3]) {
427 diff[diffOffset] = 255;
428 diff[diffOffset + 1] = 0;
429 diff[diffOffset + 2] = 0;
430 diff[diffOffset + 3] = 255;
432 diff[diffOffset] = expected[expectedOffset];
433 diff[diffOffset + 1] = expected[expectedOffset + 1];
434 diff[diffOffset + 2] = expected[expectedOffset + 2];
435 diff[diffOffset + 3] = 32;
440 diffCanvasContext.putImageData(
444 diffImageData.width, diffImageData.height);
445 diffCanvas.className = '';
448 diffCanvas.style.display = 'none';
449 $('diff-checksum').style.display = '';
450 loadTextResult(currentExpectedImageTest, 'expected-checksum');
451 loadTextResult(currentExpectedImageTest, 'actual-checksum');
455 function loadTextResult(testName, mode)
457 loadText(getTestResultUrl(testName, mode), function(text) {
458 $(mode).textContent = text;
462 function displayTextResults(testName)
464 loadTextResult(testName, 'expected-text');
465 loadTextResult(testName, 'actual-text');
466 loadTextResult(testName, 'diff-text');
471 var testSelector = $('test-selector');
472 var nextTestIndex = testSelector.selectedIndex + 1;
474 if (nextTestIndex == testSelector.options.length) {
477 if (testSelector.options[nextTestIndex].disabled) {
480 testSelector.selectedIndex = nextTestIndex;
487 function previousTest()
489 var testSelector = $('test-selector');
490 var previousTestIndex = testSelector.selectedIndex - 1;
492 if (previousTestIndex == -1) {
495 if (testSelector.options[previousTestIndex].disabled) {
498 testSelector.selectedIndex = previousTestIndex;
505 window.addEventListener('DOMContentLoaded', main);