1 // Copyright (C) 2013 Google Inc. All rights reserved.
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are
7 // * Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 // * Redistributions in binary form must reproduce the above
10 // copyright notice, this list of conditions and the following disclaimer
11 // in the documentation and/or other materials provided with the
13 // * Neither the name of Google Inc. nor the names of its
14 // contributors may be used to endorse or promote products derived from
15 // this software without specific prior written permission.
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 var history = history || {};
34 history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = {
37 testType: 'layout-tests',
41 history.validateParameter = function(state, key, value, validateFn)
47 console.log(key + ' value is not valid: ' + value);
52 history.isTreeMap = function()
54 return string.endsWith(window.location.pathname, 'treemap.html');
57 // TODO(jparent): Make private once callers move here.
58 history.queryHashAsMap = function()
60 var hash = window.location.hash;
61 var paramsList = hash ? hash.substring(1).split('&') : [];
64 for (var i = 0; i < paramsList.length; i++) {
65 var thisParam = paramsList[i].split('=');
66 if (thisParam.length != 2) {
67 console.log('Invalid query parameter: ' + paramsList[i]);
71 paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
74 // FIXME: Make master a first-class parameter instead of replacing it with the group.
75 if (paramsMap.master) {
76 var errors = new ui.Errors();
77 if (paramsMap.master == 'TryServer')
78 errors.addError('ERROR: You got here from the trybot waterfall. The try bots do not record data in the flakiness dashboard. Showing results for the regular waterfall.');
79 else if (!builders.masters[paramsMap.master] && !builders.urlNameToMasterName[paramsMap.master])
80 errors.addError('ERROR: Unknown master name: ' + paramsMap.master);
82 if (errors.hasErrors()) {
84 window.location.hash = window.location.hash.replace('master=' + paramsMap.master, '');
86 var master = builders.urlNameToMasterName[paramsMap.master] || paramsMap.master;
87 var groupIndex = master == 'ChromiumWebkit' ? 1 : 0;
88 paramsMap.group = builders.masters[master].groups[groupIndex];
89 window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + encodeURIComponent(paramsMap.group));
90 delete paramsMap.master;
94 // FIXME: Find a better way to do this. For layout-tests, we want the default group to be
95 // the ToT blink group. For other test types, we want it to be the Deps group.
96 if (!paramsMap.group && (!paramsMap.testType || paramsMap.testType == 'layout-tests'))
97 paramsMap.group = builders.groupNamesForTestType('layout-tests')[1];
102 history._diffStates = function(oldState, newState)
104 // If there is no old state, everything in the current state is new.
108 var changedParams = {};
109 for (curKey in newState) {
110 var oldVal = oldState[curKey];
111 var newVal = newState[curKey];
112 // Add new keys or changed values.
113 if (!oldVal || oldVal != newVal)
114 changedParams[curKey] = newVal;
116 return changedParams;
119 history._fillMissingValues = function(to, from)
121 for (var state in from) {
123 to[state] = from[state];
127 history.History = function(configuration)
129 this.crossDashboardState = {};
130 this.dashboardSpecificState = {};
133 this._defaultDashboardSpecificStateValues = configuration.defaultStateValues;
134 this._handleValidHashParameter = configuration.handleValidHashParameter;
135 this._handleQueryParameterChange = configuration.handleQueryParameterChange || function(historyInstance, params) { return true; };
136 this._dashboardSpecificInvalidatingParameters = configuration.invalidatingHashParameters;
137 this._generatePage = configuration.generatePage;
141 history.reloadRequiringParameters = ['showAllRuns', 'group', 'testType'];
143 var CROSS_DB_INVALIDATING_PARAMETERS = {
147 history.History.prototype = {
148 initialize: function()
150 window.onhashchange = this._handleLocationChange.bind(this);
151 this._handleLocationChange();
153 isBlinkGroup: function()
155 var group = this.crossDashboardState.group;
156 return group && group.indexOf('@ToT Blink') >= 0;
158 isLayoutTestResults: function()
160 return this.crossDashboardState.testType == 'layout-tests';
162 isGPUTestResults: function()
164 return this.crossDashboardState.testType == 'gpu_tests';
166 parseCrossDashboardParameters: function()
168 this.crossDashboardState = {};
169 var parameters = history.queryHashAsMap();
170 for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES)
171 this.parseParameter(parameters, parameterName);
173 history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
175 _parseDashboardSpecificParameters: function()
177 this.dashboardSpecificState = {};
178 var parameters = history.queryHashAsMap();
179 for (parameterName in this._defaultDashboardSpecificStateValues)
180 this.parseParameter(parameters, parameterName);
182 // TODO(jparent): Make private once callers move here.
183 parseParameters: function()
185 var oldCrossDashboardState = this.crossDashboardState;
186 var oldDashboardSpecificState = this.dashboardSpecificState;
188 this.parseCrossDashboardParameters();
190 // Some parameters require loading different JSON files when the value changes. Do a reload.
191 if (Object.keys(oldCrossDashboardState).length) {
192 for (var key in this.crossDashboardState) {
193 if (oldCrossDashboardState[key] != this.crossDashboardState[key] && history.reloadRequiringParameters.indexOf(key) != -1) {
194 window.location.reload();
200 this._parseDashboardSpecificParameters();
201 var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState);
203 history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues);
205 // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
206 if (dashboardSpecificDiffState.builder)
207 delete this.dashboardSpecificState.tests;
208 if (this.dashboardSpecificState.tests)
209 delete this.dashboardSpecificState.builder;
211 var shouldGeneratePage = true;
212 if (Object.keys(dashboardSpecificDiffState).length)
213 shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState);
214 return shouldGeneratePage;
216 // TODO(jparent): Make private once callers move here.
217 parseParameter: function(parameters, key)
219 if (!(key in parameters))
221 var value = parameters[key];
222 if (!this._handleValidHashParameterWrapper(key, value))
223 console.log("Invalid query parameter: " + key + '=' + value);
225 // Takes a key and a value and sets the this.dashboardSpecificState[key] = value iff key is
226 // a valid hash parameter and the value is a valid value for that key. Handles
227 // cross-dashboard parameters then falls back to calling
228 // handleValidHashParameter for dashboard-specific parameters.
230 // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState.
231 _handleValidHashParameterWrapper: function(key, value)
235 history.validateParameter(this.crossDashboardState, key, value,
236 function() { return builders.testTypes.indexOf(value) != -1; });
240 history.validateParameter(this.crossDashboardState, key, value,
242 return builders.getAllGroupNames().indexOf(value) != -1;
248 this.crossDashboardState[key] = value == 'true';
252 return this._handleValidHashParameter(this, key, value);
255 queryParameterValue: function(parameter)
257 return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter];
259 // Sets the page state. Takes varargs of key, value pairs.
260 setQueryParameter: function(var_args)
262 var queryParamsAsState = {};
263 for (var i = 0; i < arguments.length; i += 2) {
264 var key = arguments[i];
265 queryParamsAsState[key] = arguments[i + 1];
268 this.invalidateQueryParameters(queryParamsAsState);
270 var newState = this._combinedDashboardState();
271 for (var key in queryParamsAsState) {
272 newState[key] = queryParamsAsState[key];
275 // Note: We use window.location.hash rather that window.location.replace
276 // because of bugs in Chrome where extra entries were getting created
277 // when back button was pressed and full page navigation was occuring.
278 // FIXME: file those bugs.
279 window.location.hash = this._permaLinkURLHash(newState);
281 toggleQueryParameter: function(param)
283 this.setQueryParameter(param, !this.queryParameterValue(param));
285 invalidateQueryParameters: function(queryParamsAsState)
287 for (var key in queryParamsAsState) {
288 if (key in CROSS_DB_INVALIDATING_PARAMETERS)
289 delete this.crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]];
290 if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters)
291 delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]];
294 _joinParameters: function(stateObject)
297 for (var key in stateObject) {
298 var value = stateObject[key];
299 if (value != this._defaultValue(key))
300 state.push(key + '=' + encodeURIComponent(value));
302 return state.join('&');
304 _permaLinkURLHash: function(opt_state)
306 var state = opt_state || this._combinedDashboardState();
307 return '#' + this._joinParameters(state);
309 _combinedDashboardState: function()
311 var combinedState = Object.create(this.dashboardSpecificState);
312 for (var key in this.crossDashboardState)
313 combinedState[key] = this.crossDashboardState[key];
314 return combinedState;
316 _defaultValue: function(key)
318 if (key in this._defaultDashboardSpecificStateValues)
319 return this._defaultDashboardSpecificStateValues[key];
320 return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key];
322 _handleLocationChange: function()
324 if (this.parseParameters())
325 this._generatePage(this);