Index: dashboard/dashboard/elements/test-picker.html |
diff --git a/dashboard/dashboard/elements/test-picker.html b/dashboard/dashboard/elements/test-picker.html |
index 41f5470b5ee83ad22f7357b9236ca09eff296e3a..6dad572be15026e932a56306834dfc6d7d229f29 100644 |
--- a/dashboard/dashboard/elements/test-picker.html |
+++ b/dashboard/dashboard/elements/test-picker.html |
@@ -81,6 +81,52 @@ found in the LICENSE file. |
<script> |
'use strict'; |
+ // TODO(eakuefner): generalize this request memoization pattern. See |
+ // https://github.com/catapult-project/catapult/issues/3441. |
+ tr.exportTo('d', function() { |
+ class Subtests { |
+ constructor() { |
+ this.subtestPromises_ = new Map(); |
+ } |
+ |
+ // TODO(eakuefner): implemement cancellation by wrapping |
+ async getSubtestsForPath(path) { |
+ if (this.subtestPromises_.has(path)) { |
+ return await this.subtestPromises_.get(path); |
+ } |
+ const subtests = this.fetchAndProcessSubtests_(path); |
+ this.subtestPromises_[path] = subtests; |
+ return await subtests; |
+ } |
+ |
+ prepopulate(obj) { |
+ for (const [path, subtests] of Object.entries(obj)) { |
+ this.subtestPromises_.set(path, Promise.resolve(subtests)); |
+ } |
+ } |
+ |
+ async fetchAndProcessSubtests_(path) { |
+ const params = { |
+ type: 'pattern' |
+ }; |
+ params.p = `${path}/*`; |
+ const fullSubtests = await simple_xhr.asPromise( |
+ '/list_tests', params); |
+ // TODO(eakuefner): Standardize logic for dealing with test paths on |
+ // the client side. See |
+ // https://github.com/catapult-project/catapult/issues/3443. |
+ const subtests = []; |
+ for (const fullSubtest of fullSubtests) { |
+ subtests.push({ |
+ name: fullSubtest.substring(fullSubtest.lastIndexOf('/') + 1)}); |
+ } |
+ return subtests; |
+ } |
+ } |
+ |
+ return {Subtests}; |
+ }); |
+ |
Polymer({ |
is: 'test-picker', |
@@ -134,6 +180,11 @@ found in the LICENSE file. |
]) |
}, |
+ subtests: { |
+ type: Object, |
+ value: () => new d.Subtests() |
+ }, |
+ |
xsrfToken: { notify: true } |
}, |
@@ -146,6 +197,7 @@ found in the LICENSE file. |
this.enableAddSeries = false; |
this.selectedSuite = null; |
this.suiteDescription = null; |
+ this.updatingSubtestMenus = false; |
this.set('selectionModels.0.datalist', this.getSuiteItems()); |
}, |
@@ -227,13 +279,12 @@ found in the LICENSE file. |
onDropdownSelect: function(event) { |
var model = event.model; |
var boxIndex = model.index; |
+ if (this.updatingSubtestMenus) return; |
if (boxIndex === undefined) { |
return; |
} else if (boxIndex == 0) { |
this.updateTestSuiteDescription(); |
this.updateBotMenu(); |
- } else if (boxIndex == 1) { |
- this.sendSubtestRequest(); |
} else { |
// Update all the next dropdown menus for subtests. |
this.updateSubtestMenus(boxIndex + 1); |
@@ -262,15 +313,17 @@ found in the LICENSE file. |
/** |
* Updates bot dropdown menu with bot items. |
*/ |
- updateBotMenu: function() { |
+ updateBotMenu: async function() { |
var menu = this.getSelectionMenu(1); |
var botItems = this.getBotItems(); |
menu.set('items', botItems); |
menu.set('disabled', botItems.length === 0); |
this.subtestDict = null; |
- // If there's a selection, send a subtest request. |
+ // If there's a selection, update the subtest menus. |
if (menu.selectedItem) { |
- this.sendSubtestRequest(); |
+ this.updatingSubtestMenus = true; |
+ await this.updateSubtestMenus(2); |
+ this.updatingSubtestMenus = false; |
} else { |
// Clear all subtest menus. |
this.splice('selectionModels', 2); |
@@ -279,84 +332,52 @@ found in the LICENSE file. |
}, |
/** |
- * Sends a request for subtestDict base on selected test suite and bot. |
- */ |
- sendSubtestRequest: function() { |
- if (this.subtestXhr) { |
- this.subtestXhr.abort(); |
- this.subtestXhr = null; |
- } |
- var bot = this.getCheckedBot(); |
- // If no bot is selected, just leave the current subtests. |
- if (bot === null) { |
- return; |
- } |
- var suite = this.getCheckedSuite(); |
- if (!suite) { |
- return; |
- } |
- |
- this.loading = true; |
- |
- var params = { |
- type: 'sub_tests', |
- suite: suite, |
- bots: bot, |
- xsrf_token: this.xsrfToken |
- }; |
- this.subtestXhr = simple_xhr.send( |
- '/list_tests', |
- params, |
- function(response) { |
- this.loading = false; |
- this.subtestDict = response; |
- // Start at first subtest menu. |
- this.updateSubtestMenus(2); |
- }.bind(this), |
- function(error) { |
- // TODO: Display error. |
- this.loading = false; |
- }.bind(this) |
- ); |
- }, |
- |
- /** |
* Updates all subtest menus starting at 'startIndex'. |
*/ |
- updateSubtestMenus: function(startIndex) { |
- var subtestDict = this.getSubtestAtIndex(startIndex); |
+ updateSubtestMenus: async function(startIndex) { |
+ let subtests = await this.subtests.getSubtestsForPath( |
+ this.getCurrentSelectedPathUpTo(startIndex)); |
// Update existing subtest menu. |
for (var i = startIndex; i < this.selectionModels.length; i++) { |
- // Remove the rest of the menu if no subtestDict. |
- if (!subtestDict || Object.keys(subtestDict).length == 0) { |
+ // Remove the rest of the menu if no subtests. |
+ if (subtests.length === 0) { |
this.splice('selectionModels', i); |
this.updateAddButtonState(); |
return; |
} |
- var subtestItems = this.getSubtestItems(subtestDict); |
- var menu = this.getSelectionMenu(i); |
- menu.set('items', subtestItems); |
+ const menu = this.getSelectionMenu(i); |
+ this.updatingSubtestMenus = true; |
+ menu.set('items', subtests); |
+ this.updatingSubtestMenus = false; |
- // If there are selected item, update next menu. |
+ // If there is a selected item, update the next menu. |
if (menu.selectedItem) { |
- subtestDict = subtestDict[menu.selectedName]['sub_tests']; |
+ const selectedPath = this.getCurrentSelectedPathUpTo(i + 1, false); |
+ if (selectedPath !== undefined) { |
+ subtests = await this.subtests.getSubtestsForPath(selectedPath); |
+ } else { |
+ subtests = []; |
+ } |
} else { |
- subtestDict = null; |
+ subtests = []; |
} |
} |
- // Check if we still need to add a subtest menu. |
- if (subtestDict && Object.keys(subtestDict).length > 0) { |
- var subtestItems = this.getSubtestItems(subtestDict); |
+ // If we reached the last iteration but still have subtests, that means |
+ // that the last extant subtest selection still has subtests and we need |
+ // to put those in a new menu. |
+ if (subtests.length > 0) { |
this.push('selectionModels', { |
placeholder: this.SUBTEST_LABEL, |
- datalist: subtestItems, |
+ datalist: subtests, |
disabled: false, |
}); |
+ this.updatingSubtestMenus = true; |
Polymer.dom.flush(); |
- var menu = this.getSelectionMenu(this.selectionModels.length - 1); |
- menu.set('items', subtestItems); |
+ const menu = this.getSelectionMenu(this.selectionModels.length - 1); |
+ menu.set('items', subtests); |
+ this.updatingSubtestMenus = false; |
} |
this.updateAddButtonState(); |
@@ -367,32 +388,6 @@ found in the LICENSE file. |
(await this.getCurrentSelection()) instanceof Array); |
}, |
- getSubtestAtIndex: function(index) { |
- var subtestDict = this.subtestDict; |
- for (var i = 2; i < index; i++) { |
- var test = this.getSelectionMenu(i).selectedName; |
- if (test in subtestDict) { |
- subtestDict = subtestDict[test]['sub_tests']; |
- } else { |
- return null; |
- } |
- } |
- return subtestDict; |
- }, |
- |
- getSubtestItems: function(subtestDict) { |
- var subtestItems = []; |
- var subtestNames = Object.keys(subtestDict).sort(); |
- for (var i = 0; i < subtestNames.length; i++) { |
- var name = subtestNames[i]; |
- subtestItems.push({ |
- name: name, |
- tag: (subtestDict[name]['deprecated'] ? this.DEPRECATED_TAG : '') |
- }); |
- } |
- return subtestItems; |
- }, |
- |
getCheckedBot: function() { |
var botMenu = this.getSelectionMenu(1); |
if (botMenu.selectedItem) { |
@@ -437,29 +432,7 @@ found in the LICENSE file. |
* Gets the current selection from the menus. |
*/ |
getCurrentSelection: async function() { |
- var level = 0; |
- var parts = []; |
- while (true) { |
- var menu = this.getSelectionMenu(level); |
- if (!menu || !menu.selectedItem) { |
- // A selection is only valid if it specifies at least one subtest |
- // component, which is the third level. |
- if (level <= 2) return null; |
- break; |
- } else { |
- // We want to collect all the subtest components so we can form |
- // the full test path after this loop is done. |
- if (level >= 2) parts.push(menu.selectedItem.name); |
- } |
- level += 1; |
- } |
- |
- var suite = this.getSelectionMenu(0).selectedItem.name; |
- var bot = this.getCheckedBot(); |
- parts.unshift(suite); |
- parts.unshift(bot); |
- |
- var path = parts.join('/'); |
+ const path = this.getCurrentSelectedPathUpTo(-1, true); |
// If the paths are the same, this means that the selected path has |
// already been confirmed as valid by the previous request to |
@@ -478,7 +451,6 @@ found in the LICENSE file. |
this.getSubtestsForPath(path, true), |
this.getSubtestsForPath(path, false)]); |
} catch (e) { |
- // TODO(eakuefner): Improve this error handling. |
this.loading = false; |
return null; |
} |
@@ -486,10 +458,42 @@ found in the LICENSE file. |
this.currentSelectedPath_ = path; |
this.currentSelectedTests_ = selectedTests; |
this.currentUnselectedTests_ = unselectedTests; |
- this.updateSubtestMenus(2); |
+ this.updatingSubtestMenus = true; |
+ await this.updateSubtestMenus(2); |
+ this.updatingSubtestMenus = false; |
return selectedTests; |
}, |
+ getCurrentSelectedPathUpTo: function(maxLevel, onlyValid) { |
+ let level = 0; |
+ const parts = []; |
+ while (true) { |
+ if (maxLevel !== -1 && level >= maxLevel) { |
+ break; |
+ } |
+ const menu = this.getSelectionMenu(level); |
+ if (onlyValid && (!menu || !menu.selectedItem)) { |
+ // A selection is only valid if it specifies at least one subtest |
+ // component, which is the third level. |
+ if (level <= 2) return undefined; |
+ break; |
+ } else { |
+ // We want to collect all the subtest components so we can form |
+ // the full test path after this loop is done. |
+ if (level >= 2) parts.push(menu.selectedItem.name); |
+ } |
+ level += 1; |
+ } |
+ |
+ const suite = this.getSelectionMenu(0).selectedItem.name; |
+ const bot = this.getCheckedBot(); |
+ parts.unshift(suite); |
+ parts.unshift(bot); |
+ |
+ if (parts.length < maxLevel) return undefined; |
+ return parts.join('/'); |
+ }, |
+ |
getCurrentSelectedPath: function() { |
return this.currentSelectedPath_; |
}, |