blob: c582fbcb88e220ee2c283bc4709de5cdc332985b [file] [log] [blame]
// 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);
}
}
})();