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: remove support for mapping from the master parameter to the group
75 // one once the waterfall starts to pass in the builder name instead.
76 if (paramsMap.master) {
77 var errors = new ui.Errors();
78 if (paramsMap.master == 'TryServer')
79 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.');
80 else if (!builders.masters[paramsMap.master])
81 errors.addError('ERROR: Unknown master name: ' + paramsMap.master);
83 if (errors.hasErrors()) {
85 window.location.hash = window.location.hash.replace('master=' + paramsMap.master, '');
87 var groupIndex = paramsMap.master == 'ChromiumWebkit' ? 1 : 0;
88 paramsMap.group = builders.masters[paramsMap.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 isLayoutTestResults: function()
155 return this.crossDashboardState.testType == 'layout-tests';
157 isGPUTestResults: function()
159 return this.crossDashboardState.testType == 'gpu_tests';
161 parseCrossDashboardParameters: function()
163 this.crossDashboardState = {};
164 var parameters = history.queryHashAsMap();
165 for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES)
166 this.parseParameter(parameters, parameterName);
168 history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
170 _parseDashboardSpecificParameters: function()
172 this.dashboardSpecificState = {};
173 var parameters = history.queryHashAsMap();
174 for (parameterName in this._defaultDashboardSpecificStateValues)
175 this.parseParameter(parameters, parameterName);
177 // TODO(jparent): Make private once callers move here.
178 parseParameters: function()
180 var oldCrossDashboardState = this.crossDashboardState;
181 var oldDashboardSpecificState = this.dashboardSpecificState;
183 this.parseCrossDashboardParameters();
185 // Some parameters require loading different JSON files when the value changes. Do a reload.
186 if (Object.keys(oldCrossDashboardState).length) {
187 for (var key in this.crossDashboardState) {
188 if (oldCrossDashboardState[key] != this.crossDashboardState[key] && history.reloadRequiringParameters.indexOf(key) != -1) {
189 window.location.reload();
195 this._parseDashboardSpecificParameters();
196 var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState);
198 history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues);
200 // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
201 if (dashboardSpecificDiffState.builder)
202 delete this.dashboardSpecificState.tests;
203 if (this.dashboardSpecificState.tests)
204 delete this.dashboardSpecificState.builder;
206 var shouldGeneratePage = true;
207 if (Object.keys(dashboardSpecificDiffState).length)
208 shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState);
209 return shouldGeneratePage;
211 // TODO(jparent): Make private once callers move here.
212 parseParameter: function(parameters, key)
214 if (!(key in parameters))
216 var value = parameters[key];
217 if (!this._handleValidHashParameterWrapper(key, value))
218 console.log("Invalid query parameter: " + key + '=' + value);
220 // Takes a key and a value and sets the this.dashboardSpecificState[key] = value iff key is
221 // a valid hash parameter and the value is a valid value for that key. Handles
222 // cross-dashboard parameters then falls back to calling
223 // handleValidHashParameter for dashboard-specific parameters.
225 // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState.
226 _handleValidHashParameterWrapper: function(key, value)
230 history.validateParameter(this.crossDashboardState, key, value,
231 function() { return builders.testTypes.indexOf(value) != -1; });
235 history.validateParameter(this.crossDashboardState, key, value,
237 return builders.getAllGroupNames().indexOf(value) != -1;
243 this.crossDashboardState[key] = value == 'true';
247 return this._handleValidHashParameter(this, key, value);
250 queryParameterValue: function(parameter)
252 return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter];
254 // Sets the page state. Takes varargs of key, value pairs.
255 setQueryParameter: function(var_args)
257 var queryParamsAsState = {};
258 for (var i = 0; i < arguments.length; i += 2) {
259 var key = arguments[i];
260 queryParamsAsState[key] = arguments[i + 1];
263 this.invalidateQueryParameters(queryParamsAsState);
265 var newState = this._combinedDashboardState();
266 for (var key in queryParamsAsState) {
267 newState[key] = queryParamsAsState[key];
270 // Note: We use window.location.hash rather that window.location.replace
271 // because of bugs in Chrome where extra entries were getting created
272 // when back button was pressed and full page navigation was occuring.
273 // FIXME: file those bugs.
274 window.location.hash = this._permaLinkURLHash(newState);
276 toggleQueryParameter: function(param)
278 this.setQueryParameter(param, !this.queryParameterValue(param));
280 invalidateQueryParameters: function(queryParamsAsState)
282 for (var key in queryParamsAsState) {
283 if (key in CROSS_DB_INVALIDATING_PARAMETERS)
284 delete this.crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]];
285 if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters)
286 delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]];
289 _joinParameters: function(stateObject)
292 for (var key in stateObject) {
293 var value = stateObject[key];
294 if (value != this._defaultValue(key))
295 state.push(key + '=' + encodeURIComponent(value));
297 return state.join('&');
299 _permaLinkURLHash: function(opt_state)
301 var state = opt_state || this._combinedDashboardState();
302 return '#' + this._joinParameters(state);
304 _combinedDashboardState: function()
306 var combinedState = Object.create(this.dashboardSpecificState);
307 for (var key in this.crossDashboardState)
308 combinedState[key] = this.crossDashboardState[key];
309 return combinedState;
311 _defaultValue: function(key)
313 if (key in this._defaultDashboardSpecificStateValues)
314 return this._defaultDashboardSpecificStateValues[key];
315 return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key];
317 _handleLocationChange: function()
319 if (this.parseParameters())
320 this._generatePage(this);