/** * @fileoverview Base JS file for pages that want to parse the results JSON * from the testing bots. This deals with generic utility functions, visible * history, popups and appending the script elements for the JSON files. * * The calling page is expected to implement three "abstract" functions/objects. * generatePage, validateHashParameter and defaultStateValues. */ var pageLoadStartTime = Date.now(); /** * Generates the contents of the dashboard. The page should override this with * a function that generates the page assuming all resources have loaded. */ function generatePage() { } /** * Takes a key and a value and sets the currentState[key] = value iff key is * a valid hash parameter and the value is a valid value for that key. * * @return {boolean} Whether the key what inserted into the currentState. */ function handleValidHashParameter(key, value) { return false; } /** * Default hash parameters for the page. The page should override this to create * default states. */ var defaultStateValues = {}; ////////////////////////////////////////////////////////////////////////////// // CONSTANTS ////////////////////////////////////////////////////////////////////////////// var EXPECTATIONS_MAP = { 'T': 'TIMEOUT', 'C': 'CRASH', 'P': 'PASS', 'F': 'TEXT FAIL', 'S': 'SIMPLIFIED', 'I': 'IMAGE', 'O': 'OTHER', 'N': 'NO DATA', 'X': 'SKIP' }; // Keys in the JSON files. var WONTFIX_COUNTS_KEY = 'wontfixCounts'; var FIXABLE_COUNTS_KEY = 'fixableCounts'; var DEFERRED_COUNTS_KEY = 'deferredCounts'; var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)'; var FIXABLE_DESCRIPTION = 'All tests for this release'; var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)'; var FIXABLE_COUNT_KEY = 'fixableCount'; var ALL_FIXABLE_COUNT_KEY = 'allFixableCount'; var CHROME_REVISIONS_KEY = 'chromeRevision'; var WEBKIT_REVISIONS_KEY = 'webkitRevision'; /** * Takes a key and a value and sets the currentState[key] = value iff key is * a valid hash parameter and the value is a valid value for that key. Handles * cross-dashboard parameters then falls back to calling * handleValidHashParameter for dashboard-specific parameters. * * @return {boolean} Whether the key what inserted into the currentState. */ function handleValidHashParameterWrapper(key, value) { switch(key) { case 'testType': validateParameter(currentState, key, value, function() { return isValidName(value); }); return true; case 'debug': currentState[key] = value == 'true'; return true; default: return handleValidHashParameter(key, value); } } var defaultCrossDashboardStateValues = { testType: 'layout_test_results', debug: false } // Generic utility functions. function $(id) { return document.getElementById(id); } function stringContains(a, b) { return a.indexOf(b) != -1; } function startsWith(a, b) { return a.indexOf(b) == 0; } function isValidName(str) { return str.match(/[A-Za-z0-9\-\_,]/); } function trimString(str) { return str.replace(/^\s+|\s+$/g, ''); } /** * Naive check that path points to a directory. */ function isDirectory(path) { return !stringContains(path, '.') } function validateParameter(state, key, value, validateFn) { if (validateFn()) { state[key] = value; } else { console.log(key + ' value is not valid: ' + value); } } /** * Parses a string (e.g. window.location.hash) and calls * validValueHandler(key, value) for each key-value pair in the string. */ function parseParameters(parameterStr, validValueHandler) { var params = parameterStr.split('&'); var invalidKeys = []; for (var i = 0; i < params.length; i++) { var thisParam = params[i].split('='); if (thisParam.length != 2) { console.log('Invalid query parameter: ' + params[i]); continue; } var key = thisParam[0]; var value = decodeURIComponent(thisParam[1]); if (!validValueHandler(key, value)) invalidKeys.push(key + '=' + value); } if (invalidKeys.length) console.log("Invalid query parameters: " + invalidKeys.join(',')); } function fillMissingValues(to, from) { for (var state in from) { if (!(state in to)) to[state] = from[state]; } } function parseAllParameters() { saveStoredWindowLocation(); parseParameters(window.location.hash.substring(1), handleValidHashParameterWrapper); fillMissingValues(currentState, defaultCrossDashboardStateValues); fillMissingValues(currentState, defaultStateValues); } // Keep the location around for detecting changes to hash arguments // manually typed into the URL bar. var oldLocation; function saveStoredWindowLocation() { oldLocation = window.location.href; } function appendScript(path) { var script = document.createElement('script'); script.src = path; document.getElementsByTagName('head')[0].appendChild(script); } // Permalinkable state of the page. var currentState = {}; // Parse cross-dashboard parameters before using them. parseAllParameters(); if (currentState.debug) { // In debug mode point to the results.json and expectations.json in the // local tree. Useful for debugging changes to the python JSON generator. var builders = {'DUMMY_BUILDER_NAME': ''}; var builderBase = '../../Debug/'; } else { // Map of builderName (the name shown in the waterfall) // to builderPath (the path used in the builder's URL) // TODO(ojan): Make this switch based off of the testType. var builders = { 'Webkit': 'webkit-rel', 'Webkit (dbg)(1)': 'webkit-dbg-1', 'Webkit (dbg)(2)': 'webkit-dbg-2', 'Webkit (dbg)(3)': 'webkit-dbg-3', 'Webkit Linux': 'webkit-rel-linux', 'Webkit Linux (dbg)(1)': 'webkit-dbg-linux-1', 'Webkit Linux (dbg)(2)': 'webkit-dbg-linux-2', 'Webkit Linux (dbg)(3)': 'webkit-dbg-linux-3', 'Webkit Mac10.5': 'webkit-rel-mac5', 'Webkit Mac10.5 (dbg)(1)': 'webkit-dbg-mac5-1', 'Webkit Mac10.5 (dbg)(2)': 'webkit-dbg-mac5-2', 'Webkit Mac10.5 (dbg)(3)': 'webkit-dbg-mac5-3' }; var builderBase = 'http://build.chromium.org/buildbot/'; } // Append JSON script elements. var resultsByBuilder = {}; var expectationsByTest = {}; function ADD_RESULTS(builds) { for (var builderName in builds) { if (builderName != 'version') resultsByBuilder[builderName] = builds[builderName]; } handleLocationChange(); } function getPathToBuilderResultsFile(builderName) { return builderBase + currentState['testType'] + '/' + builders[builderName] + '/'; } var expectationsLoaded = false; function ADD_EXPECTATIONS(expectations) { expectationsLoaded = true; expectationsByTest = expectations; handleLocationChange(); } function appendJSONScriptElements() { parseAllParameters(); for (var builderName in builders) { appendScript(getPathToBuilderResultsFile(builderName) + 'results.json'); } // Grab expectations file from any builder. appendScript(getPathToBuilderResultsFile(builderName) + 'expectations.json'); } function setLoadingUIDisplayStyle(value) { if ($('loading-ui')) $('loading-ui').style.display = value; } function handleLocationChange() { setLoadingUIDisplayStyle('block'); setTimeout(function() { saveStoredWindowLocation(); parseAllParameters(); if (!expectationsLoaded) return; for (var build in builders) { if (!resultsByBuilder[build]) return; } generatePage(); setLoadingUIDisplayStyle('none'); }, 0); } /** * Sets the page state. Takes varargs of key, value pairs. * * @return list of keys modified */ function setQueryParameter(key, value) { var keys = []; for (var i = 0; i < arguments.length; i += 2) { var key = arguments[i]; keys.push(key); currentState[key] = arguments[i + 1]; } window.location.replace(getPermaLinkURL()); saveStoredWindowLocation(); return keys; } function getPermaLinkURL() { return window.location.pathname + '#' + joinParameters(currentState); } function joinParameters(stateObject) { var state = []; for (var key in stateObject) { state.push(key + '=' + encodeURIComponent(stateObject[key])); } return state.join('&'); } function logTime(msg, startTime) { console.log(msg + ': ' + (Date.now() - startTime)); } function hidePopup() { var popup = $('popup'); popup.parentNode.removeChild(popup); } function showPopup(e, html) { var popup = $('popup'); if (!popup) { popup = document.createElement('div'); popup.id = 'popup'; document.body.appendChild(popup); } // Set html first so that we can get accurate size metrics on the popup. popup.innerHTML = html; var targetRect = e.target.getBoundingClientRect(); var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth); x = Math.max(0, x); popup.style.left = x + document.body.scrollLeft + 'px'; var y = targetRect.top + targetRect.height; if (y + popup.offsetHeight > document.documentElement.clientHeight) { y = targetRect.top - popup.offsetHeight; } y = Math.max(0, y); popup.style.top = y + document.body.scrollTop + 'px'; } appendJSONScriptElements(); document.addEventListener('mousedown', function(e) { // Clear the open popup, unless the click was inside the popup. var popup = $('popup'); if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16)) { hidePopup(); } }, false); window.addEventListener('load', function() { // This doesn't seem totally accurate as there is a race between // onload firing and the last script tag being executed. logTime('Time to load JS', pageLoadStartTime); setInterval(function() { if (oldLocation != window.location.href) handleLocationChange(); }, 100); }, false);