OLD | NEW |
---|---|
1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
2 <!-- | 2 <!-- |
3 Copyright 2016 The Chromium Authors. All rights reserved. | 3 Copyright 2016 The Chromium Authors. All rights reserved. |
4 Use of this source code is governed by a BSD-style license that can be | 4 Use of this source code is governed by a BSD-style license that can be |
5 found in the LICENSE file. | 5 found in the LICENSE file. |
6 --> | 6 --> |
7 | 7 |
8 <link rel="import" href="/components/iron-flex-layout/iron-flex-layout-classes.h tml"> | 8 <link rel="import" href="/components/iron-flex-layout/iron-flex-layout-classes.h tml"> |
9 <link rel="import" href="/components/paper-button/paper-button.html"> | 9 <link rel="import" href="/components/paper-button/paper-button.html"> |
10 <link rel="import" href="/components/paper-icon-button/paper-icon-button.html"> | 10 <link rel="import" href="/components/paper-icon-button/paper-icon-button.html"> |
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
74 on-click="onAddButtonClicked" | 74 on-click="onAddButtonClicked" |
75 disabled$="{{!enableAddSeries}}">Add</paper-button> | 75 disabled$="{{!enableAddSeries}}">Add</paper-button> |
76 </div> | 76 </div> |
77 | 77 |
78 <div id="suite-description"></div> | 78 <div id="suite-description"></div> |
79 </template> | 79 </template> |
80 | 80 |
81 <script> | 81 <script> |
82 'use strict'; | 82 'use strict'; |
83 | 83 |
84 // TODO(eakuefner): generalize this request memoization pattern. See | |
85 // https://github.com/catapult-project/catapult/issues/3441. | |
86 tr.exportTo('d', function() { | |
87 class Subtests { | |
88 constructor() { | |
89 this.subtests_ = new Map(); | |
sullivan
2017/04/14 17:42:17
The naming here is still pretty confusing to me, a
eakuefner
2017/04/14 17:44:41
Done. I think that, once generalized, this will be
| |
90 } | |
91 | |
92 // TODO(eakuefner): implemement cancellation by wrapping | |
93 async getSubtestsForPath(path) { | |
94 if (this.subtests_.has(path)) { | |
95 return await this.subtests_.get(path); | |
96 } | |
97 const subtests = this.fetchAndProcessSubtests_(path); | |
98 this.subtests_[path] = subtests; | |
99 return await subtests; | |
100 } | |
101 | |
102 prepopulate(obj) { | |
103 for (const [path, subtests] of Object.entries(obj)) { | |
104 this.subtests_.set(path, Promise.resolve(subtests)); | |
105 } | |
106 } | |
107 | |
108 async fetchAndProcessSubtests_(path) { | |
109 const params = { | |
110 type: 'pattern' | |
111 }; | |
112 params.p = `${path}/*`; | |
113 const fullSubtests = await simple_xhr.asPromise( | |
114 '/list_tests', params); | |
115 // TODO(eakuefner): Standardize logic for dealing with test paths on | |
116 // the client side. See | |
117 // https://github.com/catapult-project/catapult/issues/3443. | |
118 const subtests = []; | |
119 for (const fullSubtest of fullSubtests) { | |
120 subtests.push({ | |
121 name: fullSubtest.substring(fullSubtest.lastIndexOf('/') + 1)}); | |
122 } | |
123 return subtests; | |
124 } | |
125 } | |
126 | |
127 return {Subtests}; | |
128 }); | |
129 | |
84 Polymer({ | 130 Polymer({ |
85 | 131 |
86 is: 'test-picker', | 132 is: 'test-picker', |
87 properties: { | 133 properties: { |
88 SUBTEST_LABEL: { | 134 SUBTEST_LABEL: { |
89 type: String, | 135 type: String, |
90 value: 'Subtest', | 136 value: 'Subtest', |
91 }, | 137 }, |
92 DEPRECATED_TAG: { | 138 DEPRECATED_TAG: { |
93 type: String, | 139 type: String, |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
127 disabled: false, | 173 disabled: false, |
128 }, | 174 }, |
129 { | 175 { |
130 datalist: [], | 176 datalist: [], |
131 placeholder: 'Bot', | 177 placeholder: 'Bot', |
132 disabled: true, | 178 disabled: true, |
133 } | 179 } |
134 ]) | 180 ]) |
135 }, | 181 }, |
136 | 182 |
183 subtests: { | |
184 type: Object, | |
185 value: () => new d.Subtests() | |
186 }, | |
187 | |
137 xsrfToken: { notify: true } | 188 xsrfToken: { notify: true } |
138 | 189 |
139 }, | 190 }, |
140 | 191 |
141 computeAnd: (x, y) => x && y, | 192 computeAnd: (x, y) => x && y, |
142 | 193 |
143 ready: function() { | 194 ready: function() { |
144 this.pageStateLoading = true; | 195 this.pageStateLoading = true; |
145 this.hasChart = false; | 196 this.hasChart = false; |
146 this.enableAddSeries = false; | 197 this.enableAddSeries = false; |
147 this.selectedSuite = null; | 198 this.selectedSuite = null; |
148 this.suiteDescription = null; | 199 this.suiteDescription = null; |
200 this.updatingSubtestMenus = false; | |
149 this.set('selectionModels.0.datalist', this.getSuiteItems()); | 201 this.set('selectionModels.0.datalist', this.getSuiteItems()); |
150 }, | 202 }, |
151 | 203 |
152 testSuitesChanged: function() { | 204 testSuitesChanged: function() { |
153 this.set('selectionModels.0.datalist', this.getSuiteItems()); | 205 this.set('selectionModels.0.datalist', this.getSuiteItems()); |
154 this.getSelectionMenu(0).items = this.selectionModels[0].datalist; | 206 this.getSelectionMenu(0).items = this.selectionModels[0].datalist; |
155 }, | 207 }, |
156 | 208 |
157 /** | 209 /** |
158 * Gets a list of menu items for test suites. | 210 * Gets a list of menu items for test suites. |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
220 } | 272 } |
221 return botMenuItems; | 273 return botMenuItems; |
222 }, | 274 }, |
223 | 275 |
224 /** | 276 /** |
225 * Handles dropdown menu select; updates the subsequent menu accordingly. | 277 * Handles dropdown menu select; updates the subsequent menu accordingly. |
226 */ | 278 */ |
227 onDropdownSelect: function(event) { | 279 onDropdownSelect: function(event) { |
228 var model = event.model; | 280 var model = event.model; |
229 var boxIndex = model.index; | 281 var boxIndex = model.index; |
282 if (this.updatingSubtestMenus) return; | |
230 if (boxIndex === undefined) { | 283 if (boxIndex === undefined) { |
231 return; | 284 return; |
232 } else if (boxIndex == 0) { | 285 } else if (boxIndex == 0) { |
233 this.updateTestSuiteDescription(); | 286 this.updateTestSuiteDescription(); |
234 this.updateBotMenu(); | 287 this.updateBotMenu(); |
235 } else if (boxIndex == 1) { | |
236 this.sendSubtestRequest(); | |
237 } else { | 288 } else { |
238 // Update all the next dropdown menus for subtests. | 289 // Update all the next dropdown menus for subtests. |
239 this.updateSubtestMenus(boxIndex + 1); | 290 this.updateSubtestMenus(boxIndex + 1); |
240 } | 291 } |
241 }, | 292 }, |
242 | 293 |
243 updateTestSuiteDescription: function() { | 294 updateTestSuiteDescription: function() { |
244 // Display the test suite description if there is one. | 295 // Display the test suite description if there is one. |
245 var descriptionElement = this.$['suite-description']; | 296 var descriptionElement = this.$['suite-description']; |
246 var suite = this.getSelectionMenu(0).selectedName; | 297 var suite = this.getSelectionMenu(0).selectedName; |
247 if (!this.testSuites[suite]) { | 298 if (!this.testSuites[suite]) { |
248 Polymer.dom(descriptionElement).innerHTML = ''; | 299 Polymer.dom(descriptionElement).innerHTML = ''; |
249 return; | 300 return; |
250 } | 301 } |
251 | 302 |
252 var description = this.testSuites[suite]['des']; | 303 var description = this.testSuites[suite]['des']; |
253 if (description) { | 304 if (description) { |
254 var descriptionHTML = '<b>' + suite + '</b>: '; | 305 var descriptionHTML = '<b>' + suite + '</b>: '; |
255 descriptionHTML += this.convertMarkdownLinks(description); | 306 descriptionHTML += this.convertMarkdownLinks(description); |
256 Polymer.dom(descriptionElement).innerHTML = descriptionHTML; | 307 Polymer.dom(descriptionElement).innerHTML = descriptionHTML; |
257 } else { | 308 } else { |
258 Polymer.dom(descriptionElement).innerHTML = ''; | 309 Polymer.dom(descriptionElement).innerHTML = ''; |
259 } | 310 } |
260 }, | 311 }, |
261 | 312 |
262 /** | 313 /** |
263 * Updates bot dropdown menu with bot items. | 314 * Updates bot dropdown menu with bot items. |
264 */ | 315 */ |
265 updateBotMenu: function() { | 316 updateBotMenu: async function() { |
266 var menu = this.getSelectionMenu(1); | 317 var menu = this.getSelectionMenu(1); |
267 var botItems = this.getBotItems(); | 318 var botItems = this.getBotItems(); |
268 menu.set('items', botItems); | 319 menu.set('items', botItems); |
269 menu.set('disabled', botItems.length === 0); | 320 menu.set('disabled', botItems.length === 0); |
270 this.subtestDict = null; | 321 this.subtestDict = null; |
271 // If there's a selection, send a subtest request. | 322 // If there's a selection, update the subtest menus. |
272 if (menu.selectedItem) { | 323 if (menu.selectedItem) { |
273 this.sendSubtestRequest(); | 324 this.updatingSubtestMenus = true; |
325 await this.updateSubtestMenus(2); | |
326 this.updatingSubtestMenus = false; | |
274 } else { | 327 } else { |
275 // Clear all subtest menus. | 328 // Clear all subtest menus. |
276 this.splice('selectionModels', 2); | 329 this.splice('selectionModels', 2); |
277 } | 330 } |
278 this.updateAddButtonState(); | 331 this.updateAddButtonState(); |
279 }, | 332 }, |
280 | 333 |
281 /** | 334 /** |
282 * Sends a request for subtestDict base on selected test suite and bot. | |
283 */ | |
284 sendSubtestRequest: function() { | |
285 if (this.subtestXhr) { | |
286 this.subtestXhr.abort(); | |
287 this.subtestXhr = null; | |
288 } | |
289 var bot = this.getCheckedBot(); | |
290 // If no bot is selected, just leave the current subtests. | |
291 if (bot === null) { | |
292 return; | |
293 } | |
294 var suite = this.getCheckedSuite(); | |
295 if (!suite) { | |
296 return; | |
297 } | |
298 | |
299 this.loading = true; | |
300 | |
301 var params = { | |
302 type: 'sub_tests', | |
303 suite: suite, | |
304 bots: bot, | |
305 xsrf_token: this.xsrfToken | |
306 }; | |
307 this.subtestXhr = simple_xhr.send( | |
308 '/list_tests', | |
309 params, | |
310 function(response) { | |
311 this.loading = false; | |
312 this.subtestDict = response; | |
313 // Start at first subtest menu. | |
314 this.updateSubtestMenus(2); | |
315 }.bind(this), | |
316 function(error) { | |
317 // TODO: Display error. | |
318 this.loading = false; | |
319 }.bind(this) | |
320 ); | |
321 }, | |
322 | |
323 /** | |
324 * Updates all subtest menus starting at 'startIndex'. | 335 * Updates all subtest menus starting at 'startIndex'. |
325 */ | 336 */ |
326 updateSubtestMenus: function(startIndex) { | 337 updateSubtestMenus: async function(startIndex) { |
327 var subtestDict = this.getSubtestAtIndex(startIndex); | 338 let subtests = await this.subtests.getSubtestsForPath( |
339 this.getCurrentSelectedPathUpTo(startIndex)); | |
328 | 340 |
329 // Update existing subtest menu. | 341 // Update existing subtest menu. |
330 for (var i = startIndex; i < this.selectionModels.length; i++) { | 342 for (var i = startIndex; i < this.selectionModels.length; i++) { |
331 // Remove the rest of the menu if no subtestDict. | 343 // Remove the rest of the menu if no subtests. |
332 if (!subtestDict || Object.keys(subtestDict).length == 0) { | 344 if (subtests.length === 0) { |
333 this.splice('selectionModels', i); | 345 this.splice('selectionModels', i); |
334 this.updateAddButtonState(); | 346 this.updateAddButtonState(); |
335 return; | 347 return; |
336 } | 348 } |
337 var subtestItems = this.getSubtestItems(subtestDict); | 349 const menu = this.getSelectionMenu(i); |
338 var menu = this.getSelectionMenu(i); | 350 this.updatingSubtestMenus = true; |
339 menu.set('items', subtestItems); | 351 menu.set('items', subtests); |
352 this.updatingSubtestMenus = false; | |
340 | 353 |
341 // If there are selected item, update next menu. | 354 // If there is a selected item, update the next menu. |
342 if (menu.selectedItem) { | 355 if (menu.selectedItem) { |
343 subtestDict = subtestDict[menu.selectedName]['sub_tests']; | 356 const selectedPath = this.getCurrentSelectedPathUpTo(i + 1, false); |
357 if (selectedPath !== undefined) { | |
358 subtests = await this.subtests.getSubtestsForPath(selectedPath); | |
359 } else { | |
360 subtests = []; | |
361 } | |
344 } else { | 362 } else { |
345 subtestDict = null; | 363 subtests = []; |
346 } | 364 } |
347 } | 365 } |
348 | 366 |
349 // Check if we still need to add a subtest menu. | 367 // If we reached the last iteration but still have subtests, that means |
350 if (subtestDict && Object.keys(subtestDict).length > 0) { | 368 // that the last extant subtest selection still has subtests and we need |
351 var subtestItems = this.getSubtestItems(subtestDict); | 369 // to put those in a new menu. |
370 if (subtests.length > 0) { | |
352 this.push('selectionModels', { | 371 this.push('selectionModels', { |
353 placeholder: this.SUBTEST_LABEL, | 372 placeholder: this.SUBTEST_LABEL, |
354 datalist: subtestItems, | 373 datalist: subtests, |
355 disabled: false, | 374 disabled: false, |
356 }); | 375 }); |
376 this.updatingSubtestMenus = true; | |
357 Polymer.dom.flush(); | 377 Polymer.dom.flush(); |
358 var menu = this.getSelectionMenu(this.selectionModels.length - 1); | 378 const menu = this.getSelectionMenu(this.selectionModels.length - 1); |
359 menu.set('items', subtestItems); | 379 menu.set('items', subtests); |
380 this.updatingSubtestMenus = false; | |
360 } | 381 } |
361 | 382 |
362 this.updateAddButtonState(); | 383 this.updateAddButtonState(); |
363 }, | 384 }, |
364 | 385 |
365 updateAddButtonState: async function() { | 386 updateAddButtonState: async function() { |
366 this.enableAddSeries = ( | 387 this.enableAddSeries = ( |
367 (await this.getCurrentSelection()) instanceof Array); | 388 (await this.getCurrentSelection()) instanceof Array); |
368 }, | 389 }, |
369 | 390 |
370 getSubtestAtIndex: function(index) { | |
371 var subtestDict = this.subtestDict; | |
372 for (var i = 2; i < index; i++) { | |
373 var test = this.getSelectionMenu(i).selectedName; | |
374 if (test in subtestDict) { | |
375 subtestDict = subtestDict[test]['sub_tests']; | |
376 } else { | |
377 return null; | |
378 } | |
379 } | |
380 return subtestDict; | |
381 }, | |
382 | |
383 getSubtestItems: function(subtestDict) { | |
384 var subtestItems = []; | |
385 var subtestNames = Object.keys(subtestDict).sort(); | |
386 for (var i = 0; i < subtestNames.length; i++) { | |
387 var name = subtestNames[i]; | |
388 subtestItems.push({ | |
389 name: name, | |
390 tag: (subtestDict[name]['deprecated'] ? this.DEPRECATED_TAG : '') | |
391 }); | |
392 } | |
393 return subtestItems; | |
394 }, | |
395 | |
396 getCheckedBot: function() { | 391 getCheckedBot: function() { |
397 var botMenu = this.getSelectionMenu(1); | 392 var botMenu = this.getSelectionMenu(1); |
398 if (botMenu.selectedItem) { | 393 if (botMenu.selectedItem) { |
399 let item = botMenu.selectedItem; | 394 let item = botMenu.selectedItem; |
400 return item['group'] + '/' + item['name']; | 395 return item['group'] + '/' + item['name']; |
401 } | 396 } |
402 return null; | 397 return null; |
403 }, | 398 }, |
404 | 399 |
405 getCheckedSuite: function() { | 400 getCheckedSuite: function() { |
(...skipping 24 matching lines...) Expand all Loading... | |
430 * Fires add event on 'Add' button clicked. | 425 * Fires add event on 'Add' button clicked. |
431 */ | 426 */ |
432 onAddButtonClicked: function(event, detail) { | 427 onAddButtonClicked: function(event, detail) { |
433 this.fire('add'); | 428 this.fire('add'); |
434 }, | 429 }, |
435 | 430 |
436 /** | 431 /** |
437 * Gets the current selection from the menus. | 432 * Gets the current selection from the menus. |
438 */ | 433 */ |
439 getCurrentSelection: async function() { | 434 getCurrentSelection: async function() { |
440 var level = 0; | 435 const path = this.getCurrentSelectedPathUpTo(-1, true); |
441 var parts = []; | |
442 while (true) { | |
443 var menu = this.getSelectionMenu(level); | |
444 if (!menu || !menu.selectedItem) { | |
445 // A selection is only valid if it specifies at least one subtest | |
446 // component, which is the third level. | |
447 if (level <= 2) return null; | |
448 break; | |
449 } else { | |
450 // We want to collect all the subtest components so we can form | |
451 // the full test path after this loop is done. | |
452 if (level >= 2) parts.push(menu.selectedItem.name); | |
453 } | |
454 level += 1; | |
455 } | |
456 | |
457 var suite = this.getSelectionMenu(0).selectedItem.name; | |
458 var bot = this.getCheckedBot(); | |
459 parts.unshift(suite); | |
460 parts.unshift(bot); | |
461 | |
462 var path = parts.join('/'); | |
463 | 436 |
464 // If the paths are the same, this means that the selected path has | 437 // If the paths are the same, this means that the selected path has |
465 // already been confirmed as valid by the previous request to | 438 // already been confirmed as valid by the previous request to |
466 // /list_tests. | 439 // /list_tests. |
467 // TODO(eakuefner): Update the naming of these variables to make this | 440 // TODO(eakuefner): Update the naming of these variables to make this |
468 // possible to understand without the comment. | 441 // possible to understand without the comment. |
469 if (this.currentSelectedPath_ === path) { | 442 if (this.currentSelectedPath_ === path) { |
470 return this.currentSelectedTests_; | 443 return this.currentSelectedTests_; |
471 } | 444 } |
472 | 445 |
473 this.loading = true; | 446 this.loading = true; |
474 let selectedTests; | 447 let selectedTests; |
475 let unselectedTests; | 448 let unselectedTests; |
476 try { | 449 try { |
477 [selectedTests, unselectedTests] = await Promise.all([ | 450 [selectedTests, unselectedTests] = await Promise.all([ |
478 this.getSubtestsForPath(path, true), | 451 this.getSubtestsForPath(path, true), |
479 this.getSubtestsForPath(path, false)]); | 452 this.getSubtestsForPath(path, false)]); |
480 } catch (e) { | 453 } catch (e) { |
481 // TODO(eakuefner): Improve this error handling. | |
482 this.loading = false; | 454 this.loading = false; |
483 return null; | 455 return null; |
484 } | 456 } |
485 this.loading = false; | 457 this.loading = false; |
486 this.currentSelectedPath_ = path; | 458 this.currentSelectedPath_ = path; |
487 this.currentSelectedTests_ = selectedTests; | 459 this.currentSelectedTests_ = selectedTests; |
488 this.currentUnselectedTests_ = unselectedTests; | 460 this.currentUnselectedTests_ = unselectedTests; |
489 this.updateSubtestMenus(2); | 461 this.updatingSubtestMenus = true; |
462 await this.updateSubtestMenus(2); | |
463 this.updatingSubtestMenus = false; | |
490 return selectedTests; | 464 return selectedTests; |
491 }, | 465 }, |
492 | 466 |
467 getCurrentSelectedPathUpTo: function(maxLevel, onlyValid) { | |
468 let level = 0; | |
469 const parts = []; | |
470 while (true) { | |
471 if (maxLevel !== -1 && level >= maxLevel) { | |
472 break; | |
473 } | |
474 const menu = this.getSelectionMenu(level); | |
475 if (onlyValid && (!menu || !menu.selectedItem)) { | |
476 // A selection is only valid if it specifies at least one subtest | |
477 // component, which is the third level. | |
478 if (level <= 2) return undefined; | |
479 break; | |
480 } else { | |
481 // We want to collect all the subtest components so we can form | |
482 // the full test path after this loop is done. | |
483 if (level >= 2) parts.push(menu.selectedItem.name); | |
484 } | |
485 level += 1; | |
486 } | |
487 | |
488 const suite = this.getSelectionMenu(0).selectedItem.name; | |
489 const bot = this.getCheckedBot(); | |
490 parts.unshift(suite); | |
491 parts.unshift(bot); | |
492 | |
493 if (parts.length < maxLevel) return undefined; | |
494 return parts.join('/'); | |
495 }, | |
496 | |
493 getCurrentSelectedPath: function() { | 497 getCurrentSelectedPath: function() { |
494 return this.currentSelectedPath_; | 498 return this.currentSelectedPath_; |
495 }, | 499 }, |
496 | 500 |
497 getCurrentUnselected: function() { | 501 getCurrentUnselected: function() { |
498 return this.currentUnselectedTests_; | 502 return this.currentUnselectedTests_; |
499 }, | 503 }, |
500 | 504 |
501 getSubtestsForPath: async function(path, returnSelected) { | 505 getSubtestsForPath: async function(path, returnSelected) { |
502 // TODO(eakuefner): Reimplement cancellation and memoize. | 506 // TODO(eakuefner): Reimplement cancellation and memoize. |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
551 * Converts a link in markdown format to a HTML link (anchor elements). | 555 * Converts a link in markdown format to a HTML link (anchor elements). |
552 * @param {string} text A link in markdown format. | 556 * @param {string} text A link in markdown format. |
553 * @return {string} A hyperlink string. | 557 * @return {string} A hyperlink string. |
554 */ | 558 */ |
555 convertMarkdownLinks: function(text) { | 559 convertMarkdownLinks: function(text) { |
556 return text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>'); | 560 return text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>'); |
557 } | 561 } |
558 }); | 562 }); |
559 </script> | 563 </script> |
560 </dom-module> | 564 </dom-module> |
OLD | NEW |