• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2013 Google Inc. All rights reserved.
2//
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are
5// met:
6//
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
12// distribution.
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.
16//
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.
28
29
30var history = history || {};
31
32(function() {
33
34history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = {
35    group: null,
36    showAllRuns: false,
37    testType: 'layout-tests',
38    useTestData: false,
39}
40
41history.validateParameter = function(state, key, value, validateFn)
42{
43    if (validateFn()) {
44        state[key] = value;
45        return true;
46    } else {
47        console.log(key + ' value is not valid: ' + value);
48        return false;
49    }
50}
51
52history.isTreeMap = function()
53{
54    return string.endsWith(window.location.pathname, 'treemap.html');
55}
56
57// TODO(jparent): Make private once callers move here.
58history.queryHashAsMap = function()
59{
60    var hash = window.location.hash;
61    var paramsList = hash ? hash.substring(1).split('&') : [];
62    var paramsMap = {};
63    var invalidKeys = [];
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]);
68            continue;
69        }
70
71        paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
72    }
73
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);
82
83        if (errors.hasErrors()) {
84            errors.show();
85            window.location.hash = window.location.hash.replace('master=' + paramsMap.master, '');
86        } else {
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;
91        }
92    }
93
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];
98
99    return paramsMap;
100}
101
102history._diffStates = function(oldState, newState)
103{
104    // If there is no old state, everything in the current state is new.
105    if (!oldState)
106        return newState;
107
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;
115    }
116    return changedParams;
117}
118
119history._fillMissingValues = function(to, from)
120{
121    for (var state in from) {
122        if (!(state in to))
123            to[state] = from[state];
124    }
125}
126
127history.History = function(configuration)
128{
129    this.crossDashboardState = {};
130    this.dashboardSpecificState = {};
131
132    if (configuration) {
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;
138    }
139}
140
141history.reloadRequiringParameters = ['showAllRuns', 'group', 'testType'];
142
143var CROSS_DB_INVALIDATING_PARAMETERS = {
144    'testType': 'group'
145};
146
147history.History.prototype = {
148    initialize: function()
149    {
150        window.onhashchange = this._handleLocationChange.bind(this);
151        this._handleLocationChange();
152    },
153    isLayoutTestResults: function()
154    {
155        return this.crossDashboardState.testType == 'layout-tests';
156    },
157    isGPUTestResults: function()
158    {
159        return this.crossDashboardState.testType == 'gpu_tests';
160    },
161    parseCrossDashboardParameters: function()
162    {
163        this.crossDashboardState = {};
164        var parameters = history.queryHashAsMap();
165        for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES)
166            this.parseParameter(parameters, parameterName);
167
168        history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
169    },
170    _parseDashboardSpecificParameters: function()
171    {
172        this.dashboardSpecificState = {};
173        var parameters = history.queryHashAsMap();
174        for (parameterName in this._defaultDashboardSpecificStateValues)
175            this.parseParameter(parameters, parameterName);
176    },
177    // TODO(jparent): Make private once callers move here.
178    parseParameters: function()
179    {
180        var oldCrossDashboardState = this.crossDashboardState;
181        var oldDashboardSpecificState = this.dashboardSpecificState;
182
183        this.parseCrossDashboardParameters();
184
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();
190                    return false;
191                }
192            }
193        }
194
195        this._parseDashboardSpecificParameters();
196        var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState);
197
198        history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues);
199
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;
205
206        var shouldGeneratePage = true;
207        if (Object.keys(dashboardSpecificDiffState).length)
208            shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState);
209        return shouldGeneratePage;
210    },
211    // TODO(jparent): Make private once callers move here.
212    parseParameter: function(parameters, key)
213    {
214        if (!(key in parameters))
215            return;
216        var value = parameters[key];
217        if (!this._handleValidHashParameterWrapper(key, value))
218            console.log("Invalid query parameter: " + key + '=' + value);
219    },
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.
224    //
225    // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState.
226    _handleValidHashParameterWrapper: function(key, value)
227    {
228        switch(key) {
229        case 'testType':
230            history.validateParameter(this.crossDashboardState, key, value,
231                function() { return builders.testTypes.indexOf(value) != -1; });
232            return true;
233
234        case 'group':
235            history.validateParameter(this.crossDashboardState, key, value,
236                function() {
237                    return builders.getAllGroupNames().indexOf(value) != -1;
238                });
239            return true;
240
241        case 'useTestData':
242        case 'showAllRuns':
243            this.crossDashboardState[key] = value == 'true';
244            return true;
245
246        default:
247            return this._handleValidHashParameter(this, key, value);
248        }
249    },
250    queryParameterValue: function(parameter)
251    {
252        return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter];
253    },
254    // Sets the page state. Takes varargs of key, value pairs.
255    setQueryParameter: function(var_args)
256    {
257        var queryParamsAsState = {};
258        for (var i = 0; i < arguments.length; i += 2) {
259            var key = arguments[i];
260            queryParamsAsState[key] = arguments[i + 1];
261        }
262
263        this.invalidateQueryParameters(queryParamsAsState);
264
265        var newState = this._combinedDashboardState();
266        for (var key in queryParamsAsState) {
267            newState[key] = queryParamsAsState[key];
268        }
269
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);
275    },
276    toggleQueryParameter: function(param)
277    {
278        this.setQueryParameter(param, !this.queryParameterValue(param));
279    },
280    invalidateQueryParameters: function(queryParamsAsState)
281    {
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]];
287        }
288    },
289    _joinParameters: function(stateObject)
290    {
291        var state = [];
292        for (var key in stateObject) {
293            var value = stateObject[key];
294            if (value != this._defaultValue(key))
295                state.push(key + '=' + encodeURIComponent(value));
296        }
297        return state.join('&');
298    },
299    _permaLinkURLHash: function(opt_state)
300    {
301        var state = opt_state || this._combinedDashboardState();
302        return '#' + this._joinParameters(state);
303    },
304    _combinedDashboardState: function()
305    {
306        var combinedState = Object.create(this.dashboardSpecificState);
307        for (var key in this.crossDashboardState)
308            combinedState[key] = this.crossDashboardState[key];
309        return combinedState;
310    },
311    _defaultValue: function(key)
312    {
313        if (key in this._defaultDashboardSpecificStateValues)
314            return this._defaultDashboardSpecificStateValues[key];
315        return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key];
316    },
317    _handleLocationChange: function()
318    {
319        if (this.parseParameters())
320            this._generatePage(this);
321    }
322
323}
324
325})();
326