| // Copyright (C) 2013 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 history = history || {}; |
| |
| (function() { |
| |
| history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = { |
| group: null, |
| showAllRuns: false, |
| testType: 'layout-tests', |
| useTestData: false, |
| } |
| |
| history.validateParameter = function(state, key, value, validateFn) |
| { |
| if (validateFn()) |
| state[key] = value; |
| else |
| console.log(key + ' value is not valid: ' + value); |
| } |
| |
| history.isTreeMap = function() |
| { |
| return string.endsWith(window.location.pathname, 'treemap.html'); |
| } |
| |
| // TODO(jparent): Make private once callers move here. |
| history.queryHashAsMap = function() |
| { |
| var hash = window.location.hash; |
| var paramsList = hash ? hash.substring(1).split('&') : []; |
| var paramsMap = {}; |
| var invalidKeys = []; |
| for (var i = 0; i < paramsList.length; i++) { |
| var thisParam = paramsList[i].split('='); |
| if (thisParam.length != 2) { |
| console.log('Invalid query parameter: ' + paramsList[i]); |
| continue; |
| } |
| |
| paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]); |
| } |
| |
| // FIXME: remove support for mapping from the master parameter to the group |
| // one once the waterfall starts to pass in the builder name instead. |
| if (paramsMap.master) { |
| paramsMap.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[paramsMap.master]; |
| if (!paramsMap.group) |
| console.log('ERROR: Unknown master name: ' + paramsMap.master); |
| window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + paramsMap.group); |
| delete paramsMap.master; |
| } |
| |
| return paramsMap; |
| } |
| |
| history._diffStates = function(oldState, newState) |
| { |
| // If there is no old state, everything in the current state is new. |
| if (!oldState) |
| return newState; |
| |
| var changedParams = {}; |
| for (curKey in newState) { |
| var oldVal = oldState[curKey]; |
| var newVal = newState[curKey]; |
| // Add new keys or changed values. |
| if (!oldVal || oldVal != newVal) |
| changedParams[curKey] = newVal; |
| } |
| return changedParams; |
| } |
| |
| history._fillMissingValues = function(to, from) |
| { |
| for (var state in from) { |
| if (!(state in to)) |
| to[state] = from[state]; |
| } |
| } |
| |
| history.History = function(configuration) |
| { |
| this.crossDashboardState = {}; |
| this.dashboardSpecificState = {}; |
| |
| if (configuration) { |
| this._defaultDashboardSpecificStateValues = configuration.defaultStateValues; |
| this._handleValidHashParameter = configuration.handleValidHashParameter; |
| this._handleQueryParameterChange = configuration.handleQueryParameterChange || function(historyInstance, params) { return true; }; |
| this._dashboardSpecificInvalidatingParameters = configuration.invalidatingHashParameters; |
| this._generatePage = configuration.generatePage; |
| } |
| } |
| |
| var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType']; |
| |
| var CROSS_DB_INVALIDATING_PARAMETERS = { |
| 'testType': 'group' |
| }; |
| |
| history.History.prototype = { |
| initialize: function() |
| { |
| window.onhashchange = this._handleLocationChange.bind(this); |
| this._handleLocationChange(); |
| }, |
| isLayoutTestResults: function() |
| { |
| return this.crossDashboardState.testType == 'layout-tests'; |
| }, |
| parseCrossDashboardParameters: function() |
| { |
| this.crossDashboardState = {}; |
| var parameters = history.queryHashAsMap(); |
| for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES) |
| this.parseParameter(parameters, parameterName); |
| |
| history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES); |
| }, |
| _parseDashboardSpecificParameters: function() |
| { |
| this.dashboardSpecificState = {}; |
| var parameters = history.queryHashAsMap(); |
| for (parameterName in this._defaultDashboardSpecificStateValues) |
| this.parseParameter(parameters, parameterName); |
| }, |
| // TODO(jparent): Make private once callers move here. |
| parseParameters: function() |
| { |
| var oldCrossDashboardState = this.crossDashboardState; |
| var oldDashboardSpecificState = this.dashboardSpecificState; |
| |
| this.parseCrossDashboardParameters(); |
| |
| // Some parameters require loading different JSON files when the value changes. Do a reload. |
| if (Object.keys(oldCrossDashboardState).length) { |
| for (var key in this.crossDashboardState) { |
| if (oldCrossDashboardState[key] != this.crossDashboardState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1) { |
| window.location.reload(); |
| return false; |
| } |
| } |
| } |
| |
| this._parseDashboardSpecificParameters(); |
| var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState); |
| |
| history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues); |
| |
| // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys. |
| if (dashboardSpecificDiffState.builder) |
| delete this.dashboardSpecificState.tests; |
| if (this.dashboardSpecificState.tests) |
| delete this.dashboardSpecificState.builder; |
| |
| var shouldGeneratePage = true; |
| if (Object.keys(dashboardSpecificDiffState).length) |
| shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState); |
| return shouldGeneratePage; |
| }, |
| // TODO(jparent): Make private once callers move here. |
| parseParameter: function(parameters, key) |
| { |
| if (!(key in parameters)) |
| return; |
| var value = parameters[key]; |
| if (!this._handleValidHashParameterWrapper(key, value)) |
| console.log("Invalid query parameter: " + key + '=' + value); |
| }, |
| // Takes a key and a value and sets the this.dashboardSpecificState[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 this.dashboardSpecificState. |
| _handleValidHashParameterWrapper: function(key, value) |
| { |
| switch(key) { |
| case 'testType': |
| history.validateParameter(this.crossDashboardState, key, value, |
| function() { return TEST_TYPES.indexOf(value) != -1; }); |
| return true; |
| |
| case 'group': |
| history.validateParameter(this.crossDashboardState, key, value, |
| function() { |
| return value in LAYOUT_TESTS_BUILDER_GROUPS; |
| }); |
| return true; |
| |
| case 'useTestData': |
| case 'showAllRuns': |
| this.crossDashboardState[key] = value == 'true'; |
| return true; |
| |
| default: |
| return this._handleValidHashParameter(this, key, value); |
| } |
| }, |
| queryParameterValue: function(parameter) |
| { |
| return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter]; |
| }, |
| // Sets the page state. Takes varargs of key, value pairs. |
| setQueryParameter: function(var_args) |
| { |
| var queryParamsAsState = {}; |
| for (var i = 0; i < arguments.length; i += 2) { |
| var key = arguments[i]; |
| queryParamsAsState[key] = arguments[i + 1]; |
| } |
| |
| this.invalidateQueryParameters(queryParamsAsState); |
| |
| var newState = this._combinedDashboardState(); |
| for (var key in queryParamsAsState) { |
| newState[key] = queryParamsAsState[key]; |
| } |
| |
| // Note: We use window.location.hash rather that window.location.replace |
| // because of bugs in Chrome where extra entries were getting created |
| // when back button was pressed and full page navigation was occuring. |
| // FIXME: file those bugs. |
| window.location.hash = this._permaLinkURLHash(newState); |
| }, |
| toggleQueryParameter: function(param) |
| { |
| this.setQueryParameter(param, !this.queryParameterValue(param)); |
| }, |
| invalidateQueryParameters: function(queryParamsAsState) |
| { |
| for (var key in queryParamsAsState) { |
| if (key in CROSS_DB_INVALIDATING_PARAMETERS) |
| delete this.crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]]; |
| if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters) |
| delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]]; |
| } |
| }, |
| _joinParameters: function(stateObject) |
| { |
| var state = []; |
| for (var key in stateObject) { |
| var value = stateObject[key]; |
| if (value != this._defaultValue(key)) |
| state.push(key + '=' + encodeURIComponent(value)); |
| } |
| return state.join('&'); |
| }, |
| _permaLinkURLHash: function(opt_state) |
| { |
| var state = opt_state || this._combinedDashboardState(); |
| return '#' + this._joinParameters(state); |
| }, |
| _combinedDashboardState: function() |
| { |
| var combinedState = Object.create(this.dashboardSpecificState); |
| for (var key in this.crossDashboardState) |
| combinedState[key] = this.crossDashboardState[key]; |
| return combinedState; |
| }, |
| _defaultValue: function(key) |
| { |
| if (key in this._defaultDashboardSpecificStateValues) |
| return this._defaultDashboardSpecificStateValues[key]; |
| return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key]; |
| }, |
| _handleLocationChange: function() |
| { |
| if (this.parseParameters()) |
| this._generatePage(this); |
| } |
| |
| } |
| |
| })(); |