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