1/** 2 * Copyright 2018 The WPT Dashboard Project. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 */ 6 7import '../components/info-banner.js'; 8import { LoadingState } from '../components/loading-state.js'; 9import '../components/path.js'; 10import '../components/test-file-results.js'; 11import '../components/test-results-history-timeline.js'; 12import '../components/test-run.js'; 13import '../components/test-runs-query-builder.js'; 14import { TestRunsUIBase } from '../components/test-runs.js'; 15import '../components/test-search.js'; 16import { WPTColors } from '../components/wpt-colors.js'; 17import { WPTFlags } from '../components/wpt-flags.js'; 18import '../components/wpt-permalinks.js'; 19import '../components/wpt-metadata.js'; 20import { AmendMetadataMixin } from '../components/wpt-amend-metadata.js'; 21import '../node_modules/@polymer/iron-collapse/iron-collapse.js'; 22import '../node_modules/@polymer/iron-icon/iron-icon.js'; 23import '../node_modules/@polymer/iron-icons/editor-icons.js'; 24import '../node_modules/@polymer/iron-icons/image-icons.js'; 25import '../node_modules/@polymer/paper-button/paper-button.js'; 26import '../node_modules/@polymer/paper-icon-button/paper-icon-button.js'; 27import '../node_modules/@polymer/paper-spinner/paper-spinner-lite.js'; 28import '../node_modules/@polymer/paper-styles/color.js'; 29import '../node_modules/@polymer/paper-tabs/paper-tabs.js'; 30import '../node_modules/@polymer/paper-toast/paper-toast.js'; 31import '../node_modules/@polymer/polymer/lib/elements/dom-if.js'; 32import '../node_modules/@polymer/polymer/lib/elements/dom-repeat.js'; 33import '../node_modules/@polymer/polymer/polymer-element.js'; 34import { html } from '../node_modules/@polymer/polymer/polymer-element.js'; 35import { PathInfo } from '../components/path.js'; 36import { Pluralizer } from '../components/pluralize.js'; 37 38const TEST_TYPES = ['manual', 'reftest', 'testharness', 'visual', 'wdspec', 'test262']; 39 40// Map of abbreviations for status values stored in summary files. 41// This is used to expand the status to its full value after being 42// abbreviated for smaller storage in summary files. 43// NOTE: If a new status abbreviation is added here, the mapping 44// at results_processor/wptreport.py will also require the change. 45const STATUS_ABBREVIATIONS = { 46 'P': 'PASS', 47 'O': 'OK', 48 'F': 'FAIL', 49 'S': 'SKIP', 50 'E': 'ERROR', 51 'N': 'NOTRUN', 52 'C': 'CRASH', 53 'T': 'TIMEOUT', 54 'PF': 'PRECONDITION_FAILED' 55}; 56const PASSING_STATUSES = ['O', 'P']; 57 58// VIEW_ENUM contains the different values for the `view` query parameter. 59export const VIEW_ENUM = { 60 Subtest: 'subtest', 61 Interop: 'interop', 62 Test: 'test' 63} 64 65/** 66 * Determines if a test result for view=test should be considered a "PASS". 67 * This function is defined outside the WPTResults class to avoid 'this' binding issues. 68 * For example, if this function were a method of WPTResults and passed as a callback 69 * to another function (e.g., within a loop), the 'this' context within the callback 70 * might not refer to the WPTResults instance, leading to errors when trying to 71 * access component properties or methods. 72 * @param {number} total - The total number of subtests. 73 * @param {number} passes - The number of passing subtests. 74 * @param {string | undefined} status - The status of the test. 75 * @returns {boolean} 76 */ 77function isViewTestPass(total, passes, status) { 78 return (passes === total && !isTestMissing(total, status) && ( 79 (status === undefined) || (status === '') || (PASSING_STATUSES.includes(status)) 80 )); 81} 82 83/** 84 * Determines if a test result is missing. 85 * This function is defined outside the WPTResults class to avoid 'this' binding issues. 86 * For example, if this function were a method of WPTResults and passed as a callback 87 * to another function (e.g., within a loop), the 'this' context within the callback 88 * might not refer to the WPTResults instance, leading to errors when trying to 89 * access component properties or methods. 90 * @param {number} total - The total number of subtests. 91 * @param {string | undefined} status - The status of the test. 92 * @returns {boolean} 93 */ 94function isTestMissing(total, status) { 95 return status === '' && total === 0 96} 97 98class WPTResults extends AmendMetadataMixin(Pluralizer(WPTColors(WPTFlags(PathInfo(LoadingState(TestRunsUIBase)))))) { 99 static get template() { 100 return html` 101 <style include="wpt-colors"> 102 :host { 103 display: block; 104 font-size: 15px; 105 } 106 table { 107 width: 100%; 108 border-collapse: collapse; 109 } 110 tr:nth-child(2n), tr.spec { 111 background-color: var(--paper-grey-200); 112 } 113 tr td { 114 padding: 0.25em 0.5em; 115 } 116 tr.spec td { 117 padding: 0.2em 0.5em; 118 border: solid 1px var(--paper-grey-300); 119 } 120 thead { 121 border-bottom: 8px solid white; 122 } 123 th { 124 background: white; 125 position: sticky; 126 top: 0; 127 z-index: 1; 128 } 129 path-part { 130 vertical-align: bottom; 131 } 132 .path { 133 margin-bottom: 16px; 134 } 135 .path-separator { 136 padding: 0 0.1em; 137 margin: 0 0.2em; 138 } 139 .top, 140 .delta { 141 background-color: var(--paper-grey-200); 142 } 143 span.delta.regressions { 144 color: var(--paper-red-700); 145 } 146 span.delta.passes { 147 color: var(--paper-green-700); 148 } 149 td.none { 150 visibility: hidden; 151 } 152 td.numbers { 153 white-space: nowrap; 154 color: black; 155 } 156 td[triage] { 157 cursor: pointer; 158 } 159 td[triage]:hover { 160 opacity: 0.7; 161 box-shadow: 5px 5px 5px; 162 } 163 td[selected] { 164 border: 2px solid #000000; 165 } 166 .totals-row {
167 border-top: 4px solid white; 168 padding: 4px; 169 } 170 .yellow-button { 171 color: var(--paper-yellow-500); 172 margin-left: 32px; 173 } 174 .history { 175 margin: 32px 0; 176 text-align: center; 177 } 178 .history h3 span { 179 color: var(--paper-red-500); 180 } 181 #show-history { 182 background: var(--paper-blue-700); 183 color: white; 184 } 185 .test-type { 186 margin-left: 8px; 187 padding: 4px; 188 border-radius: 4px; 189 background-color: var(--paper-blue-100); 190 } 191 @media (max-width: 1200px) { 192 table tr td:first-child::after { 193 content: ""; 194 display: inline-block; 195 vertical-align: top; 196 min-height: 30px; 197 } 198 } 199 .sort-col { 200 border-top: 4px solid white; 201 padding: 4px; 202 } 203 .sort-button { 204 margin-left: -15px; 205 } 206 .view-triage { 207 margin-left: 30px; 208 } 209 .pointer { 210 cursor: help; 211 } 212 213 .channel-area { 214 display: flex; 215 max-width: fit-content; 216 margin-inline: auto; 217 border-radius: 3px; 218 margin-bottom:20px; 219 box-shadow: var(--shadow-elevation-2dp_-_box-shadow); 220 } 221 222 .channel-area > paper-button { 223 margin: 0; 224 } 225 226 .channel-area > paper-button:first-of-type { 227 border-top-right-radius: 0; 228 border-bottom-right-radius: 0; 229 } 230 231 .channel-area > paper-button:last-of-type { 232 border-top-left-radius: 0; 233 border-bottom-left-radius: 0; 234 } 235 .unselected { 236 background-color: white; 237 } 238 .selected { 239 background-color: var(--paper-blue-700); 240 color: white; 241 } 242 243 .selected::before { 244 --_size: 1rem; 245 --_half-size: calc(var(--_size) / 2); 246 247 content: ""; 248 position: absolute; 249 bottom: calc(var(--_half-size) * -1 + 1px); 250 width: var(--_size); 251 height: var(--_half-size); 252 left: calc(50% - var(--_half-size)); 253 background: var(--paper-blue-700); 254 clip-path: polygon(46% 100%, 0 0, 100% 0); 255 } 256 </style> 257 258 <paper-toast id="selected-toast" duration="0"> 259 <span>[[triageToastMsg(selectedMetadata.length)]]</span> 260 <paper-button class="view-triage" on-click="openAmendMetadata" raised="[[hasSelections]]" disabled="[[!hasSelections]]">TRIAGE</paper-button> 261 </paper-toast> 262 263 <template is="dom-if" if="[[isInvalidDiffUse(diff, testRuns)]]"> 264 <paper-toast id="diffInvalid" duration="0" text="'diff' was requested, but is only valid when comparing two runs." opened> 265 <paper-button onclick="[[dismissToast]]" class="yellow-button">Close</paper-button> 266 </paper-toast> 267 </template> 268 269 <paper-toast id="runsNotInCache" duration="5000" text="One or more of the runs requested is currently being loaded into the cache. Trying again..."></paper-toast> 270 271 <template is="dom-if" if="[[resultsLoadFailed]]"> 272 <info-banner type="error"> 273 Failed to fetch test runs. 274 </info-banner> 275 </template> 276 277 <iron-collapse opened="[[editingQuery]]"> 278 <test-runs-query-builder query="[[query]]" 279 on-submit="[[submitQuery]]"> 280 </test-runs-query-builder> 281 </iron-collapse> 282 283 <template is="dom-if" if="[[testRuns]]"> 284 <template is="dom-if" if="{{ pathIsATestFile }}"> 285 <test-file-results test-runs="[[testRuns]]" 286 subtest-row-count={{subtestRowCount}} 287 path="[[path]]" 288 structured-search="[[structuredSearch]]" 289 labels="[[labels]]" 290 products="[[products]]" 291 diff-run="[[diffRun]]" 292 is-triage-mode="[[isTriageMode]]" 293 metadata-map="[[metadataMap]]"> 294 </test-file-results> 295 </template> 296 <template is="dom-if" if="[[shouldDisplayToggle(canViewInteropScores, pathIsATestFile)]]"> 297 <div class="channel-area"> 298 <paper-button id="toggleInterop" class\$="[[ interopButtonClass(view) ]]" on-click="clickInterop">Interop View</paper-button> 299 <paper-button id="toggleDefault" class\$="[[ defaultButtonClass(view) ]]" on-click="clickDefault">Default View</paper-button> 300 </div> 301 </template> 302 303 <template is="dom-if" if="{{ !pathIsATestFile }}"> 304 <table> 305 <thead> 306 <tr> 307 <th>Path</th> 308 <template is="dom-repeat" items="[[testRuns]]" as="testRun"> 309 <!-- Repeats for as many different browser test runs are available --> 310 <th><test-run test-run="[[testRun]]" show-source show-platform></test-run></th> 311 </template> 312 <template is="dom-if" if="[[diffRun]]"> 313 <th> 314 <test-run test-run="[[diffRun]]"></test-run> 315 <paper-icon-button icon="filter-list" onclick="[[toggleDiffFilter]]" title="Toggle filtering to only show differences"></paper-icon-button> 316 </th> 317 </template> 318 </tr> 319 </thead> 320 321 <tbody> 322 <template is="dom-if" if="[[displayedNodes]]"> 323 <tr class="sort-col"> 324 <td> 325 <paper-icon-button class="sort-button" src=[[getSortIcon(isPathSorted)]] onclick="[[sortTestName]]" aria-label="Sort the test name column"></paper-icon-button> 326 </td>
327 <template is="dom-repeat" items="[[sortCol]]" as="sortItem"> 328 <td> 329 <paper-icon-button class="sort-button" src=[[getSortIcon(sortItem)]] onclick="[[sortTestResults(index)]]" aria-label="Sort the test result column"></paper-icon-button> 330 </td> 331 </template> 332 </tr> 333 </template> 334 335 <template is="dom-repeat" items="{{displayedNodes}}" as="node"> 336 <tr> 337 <td onclick="[[handleTriageSelect(null, node, testRun)]]" onmouseover="[[handleTriageHover(null, node, testRun)]]"> 338 <path-part 339 prefix="/results" 340 path="[[ node.path ]]" 341 query="{{ query }}" 342 is-dir="{{ node.isDir }}" 343 is-triage-mode=[[isTriageMode]]> 344 </path-part> 345 <template is="dom-if" if="[[shouldDisplayMetadata(null, node.path, metadataMap)]]"> 346 <a href="[[ getMetadataUrl(null, node.path, metadataMap) ]]" target="_blank"><iron-icon class="bug" icon="bug-report"></iron-icon></a> 347 </template> 348 <template is="dom-if" if="[[shouldDisplayTestLabel(node.path, labelMap)]]"> 349 <iron-icon class="bug" icon="label" title="[[getTestLabelTitle(node.path, labelMap)]]"></iron-icon> 350 </template> 351 </td> 352 353 <template is="dom-repeat" items="[[testRuns]]" as="testRun"> 354 <td class\$="numbers [[ testResultClass(node, index, testRun, 'passes') ]]" onclick="[[handleTriageSelect(index, node, testRun)]]" onmouseover="[[handleTriageHover(index, node, testRun)]]"> 355 <template is="dom-if" if="[[diffRun]]"> 356 <span class\$="passes [[ testResultClass(node, index, testRun, 'passes') ]]">{{ getNodeResultDataByPropertyName(node, index, testRun, 'subtest_passes') }}</span> 357 / 358 <span class\$="total [[ testResultClass(node, index, testRun, 'total') ]]">{{ getNodeResultDataByPropertyName(node, index, testRun, 'subtest_total') }}</span> 359 </template> 360 <template is="dom-if" if="[[!diffRun]]"> 361 <span class\$="passes [[ testResultClass(node, index, testRun, 'passes') ]]">{{ getNodeResult(node, index) }}</span> 362 <template is="dom-if" if="[[ shouldDisplayHarnessWarning(node, index) ]]"> 363 <span class="pointer" title\$="Harness [[ getStatusDisplay(node, index) ]]"> â ï¸</span> 364 </template> 365 </template> 366 <template is="dom-if" if="[[shouldDisplayMetadata(index, node.path, metadataMap)]]"> 367 <a href="[[ getMetadataUrl(index, node.path, metadataMap) ]]" target="_blank"><iron-icon class="bug" icon="bug-report"></iron-icon></a> 368 </template> 369 </td> 370 </template> 371 372 <template is="dom-if" if="[[diffRun]]"> 373 <td class\$="numbers [[ testResultClass(node, index, diffRun, 'passes') ]]"> 374 <template is="dom-if" if="[[node.diff]]"> 375 <span class="delta passes">{{ getNodeResultDataByPropertyName(node, -1, diffRun, 'passes') }}</span> 376 / 377 <span class="delta regressions">{{ getNodeResultDataByPropertyName(node, -1, diffRun, 'regressions') }}</span> 378 / 379 <span class="delta total">{{ getNodeResultDataByPropertyName(node, -1, diffRun, 'total') }}</span> 380 </template> 381 </td> 382 </template> 383 </tr> 384 </template> 385 386 <template is="dom-if" if="[[ shouldDisplayTotals(displayedTotals, diffRun) ]]"> 387 <tr class="totals-row"> 388 <td> 389 <code><strong>[[getTotalText()]]</strong></code> 390 </td> 391 <template is="dom-repeat" items="[[displayedTotals]]" as="columnTotal"> 392 <td class\$="numbers [[ getTotalsClass(columnTotal) ]]">
393 <span class\$="total [[ getTotalsClass(columnTotal) ]]">{{ getTotalDisplay(columnTotal) }}</span> 394 </td> 395 </template> 396 </tr> 397 </template> 398 </tbody> 399 </table> 400 401 <template is="dom-if" if="[[noResults]]"> 402 <info-banner type="info"> 403 No results. 404 </info-banner> 405 </template> 406 </template> 407 </template> 408 409 <template is="dom-if" if="[[pathIsATestFile]]"> 410 <div class="history"> 411 <template is="dom-if" if="[[!showHistory]]"> 412 <paper-button id="show-history" onclick="[[showHistoryClicked()]]" raised> 413 Show history timeline 414 </paper-button> 415 </template> 416 <template is="dom-if" if="[[showHistory]]"> 417 <h3> 418 History: 419 </h3> 420 <template is="dom-if" if="[[pathIsATestFile]]"> 421 <test-results-history-timeline 422 path="[[path]]" 423 show-test-history="[[showHistory]]" 424 subtest-names="[[subtestNames]]"> 425 </test-results-history-timeline> 426 </template> 427 </template> 428 </div> 429 </template> 430 431 <wpt-metadata products="[[displayedProducts]]" 432 path="[[path]]" 433 search-results="[[searchResults]]" 434 metadata-map="[[metadataMap]]" 435 label-map="[[labelMap}]]" 436 triage-notifier="[[triageNotifier]]"></wpt-metadata> 437 <wpt-amend-metadata id="amend" selected-metadata="[[selectedMetadata]]" path="[[path]]"></wpt-amend-metadata> 438`; 439 } 440 441 static get is() { 442 return 'wpt-results'; 443 } 444 445 static get properties() { 446 return { 447 path: { 448 type: String, 449 observer: 'pathUpdated', 450 notify: true, 451 }, 452 pathIsASubfolderOrFile: { 453 type: Boolean, 454 computed: 'computePathIsASubfolderOrFile(pathIsASubfolder, pathIsATestFile)' 455 }, 456 structuredSearch: Object, 457 searchResults: { 458 type: Array, 459 value: [], 460 notify: true, 461 }, 462 subtestRowCount: { 463 type: Number, 464 notify: true 465 }, 466 testPaths: { 467 type: Set, 468 computed: 'computeTestPaths(searchResults)', 469 notify: true, 470 }, 471 displayedNodes: { 472 type: Array, 473 value: [], 474 }, 475 displayedTests: { 476 type: Array, 477 computed: 'computeDisplayedTests(path, searchResults)', 478 }, 479 displayedTotals: { 480 type: Array, 481 value: [], 482 }, 483 metadataMap: Object, 484 labelMap: Object, 485 // Users request to show a diff column. 486 diff: Boolean, 487 diffRun: { 488 type: Object, 489 value: null, 490 }, 491 diffURL: { 492 type: String, 493 computed: 'computeDiffURL(testRuns)', 494 }, 495 showHistory: { 496 type: Boolean, 497 value: false, 498 }, 499 subtestNames: { 500 type: Array, 501 value:[] 502 }, 503 resultsLoadFailed: Boolean, 504 noResults: Boolean, 505 editingQuery: { 506 type: Boolean, 507 value: false, 508 }, 509 sortCol: { 510 type: Array, 511 value: [], 512 }, 513 isPathSorted: { 514 type: Boolean, 515 value: false, 516 }, 517 canViewInteropScores: { 518 type: Boolean, 519 value: false 520 }, 521 onlyShowDifferences: Boolean, 522 // path => {type, file[, refPath]} simplification. 523 screenshots: Array, 524 triageNotifier: Boolean, 525 }; 526 } 527 528 static get observers() { 529 return [ 530 'clearSelectedCells(selectedMetadata)', 531 'handleTriageMode(isTriageMode)', 532 'changeView(view)' 533 ]; 534 } 535 536 isInvalidDiffUse(diff, testRuns) { 537 return diff && testRuns && testRuns.length !== 2; 538 } 539 540 computePathIsASubfolderOrFile(isSubfolder, isFile) { 541 return isSubfolder || isFile; 542 } 543 544 computeTestPaths(searchResults) { 545 const paths = searchResults && searchResults.map(r => r.test) || []; 546 return new Set(paths); 547 } 548 549 computeDisplayedTests(path, searchResults) { 550 return searchResults 551 && searchResults.map(r => r.test).filter(name => name.startsWith(path)) 552 || []; 553 } 554 555 computeDiffURL(testRuns) { 556 if (!testRuns || testRuns.length !== 2) { 557 return; 558 } 559 let url = new URL('/api/diff', window.location); 560 for (const run of testRuns) { 561 url.searchParams.append('run_id', run.id); 562 } 563 url.searchParams.set('filter', this.diffFilter); 564 return url; 565 } 566 567 constructor() { 568 super(); 569 this.onLoadingComplete = () => { 570 this.noResults = !this.resultsLoadFailed 571 && !(this.searchResults && this.searchResults.length); 572 }; 573 this.toggleQueryEdit = () => { 574 this.editingQuery = !this.editingQuery; 575 }; 576 this.toggleDiffFilter = () => { 577 this.onlyShowDifferences = !this.onlyShowDifferences; 578 this.refreshDisplayedNodes(); 579 }; 580 this.dismissToast = e => e.target.closest('paper-toast').close(); 581 this.reloadPendingMetadata = this.handleReloadPendingMetadata.bind(this); 582 this.sortTestName = this.sortTestName.bind(this); 583 this.addEventListener('selected-metadata-changed', this.selectedMetadataChanged); 584 this.addEventListener('metadata-map-changed', this.metadataMapChanged); 585 this.addEventListener('label-map-changed', this.labelMapChanged); 586 } 587 588 connectedCallback() { 589 super.connectedCallback();
590 this.addEventListener('triagemetadata', this.reloadPendingMetadata); 591 this.addEventListener('subtestrows', this.handleGetSubtestRows); 592 } 593 594 disconnectedCallback() { 595 this.removeEventListener('triagemetadata', this.reloadPendingMetadata); 596 super.disconnectedCallback(); 597 } 598 599 loadData() { 600 this.resultsLoadFailed = false; 601 this.load( 602 this.loadRuns().then(async runs => { 603 // Pass current (un)structured query is passed to fetchResults(). 604 this.fetchResults(this.structuredSearch || this.search); 605 606 // Load a diff data into this.diffRun, if needed. 607 if (this.diff && runs && runs.length === 2) { 608 this.diffRun = { 609 revision: 'diff', 610 browser_name: 'diff', 611 }; 612 this.fetchDiff(); 613 } 614 }), 615 () => { 616 this.resultsLoadFailed = true; 617 } 618 ); 619 } 620 621 reloadData() { 622 if (!this.diff) { 623 this.diffRun = null; 624 } 625 this.testRuns = []; 626 this.sortCol = []; 627 this.searchResults = []; 628 this.displayedTotals = []; 629 this.refreshDisplayedNodes(); 630 this.loadData(); 631 } 632 633 handleGetSubtestRows(event) { 634 this.subtestNames = event.detail.rows.map(subtestRow => { 635 // The overall test status is given as an empty string. 636 if(subtestRow.name === 'Harness status' || subtestRow.name === 'Test status') { 637 return ''; 638 } 639 return subtestRow.name.replace(/\s/g, ' '); 640 }).filter(subtestName => subtestName !== 'Duration') 641 } 642 643 fetchResults(q) { 644 if (!this.testRuns || !this.testRuns.length) { 645 return; 646 } 647 648 let url = new URL('/api/search', window.location); 649 let fetchOpts; 650 651 const body = { 652 run_ids: this.testRuns.map(r => r.id), 653 }; 654 if (q) { 655 body.query = q; 656 } 657 if (this.diff) { 658 url.searchParams.set('diff', true); 659 url.searchParams.set('filter', this.diffFilter); 660 } 661 fetchOpts = { 662 method: 'POST', 663 body: JSON.stringify(body), 664 }; 665 this.sortCol = new Array(this.testRuns.length).fill(false); 666 667 // Fetch search results and refresh display nodes. If fetch error is HTTP' 668 // 422, expect backend to attempt write-on-read of missing data. In such 669 // cases, retry fetch up to 5 times with 5000ms waits in between. 670 const toast = this.shadowRoot.querySelector('#runsNotInCache'); 671 this.load( 672 this.retry( 673 async() => { 674 const r = await window.fetch(url, fetchOpts); 675 if (!r.ok) { 676 if (fetchOpts.method === 'POST' && r.status === 422) { 677 toast.open(); 678 throw r.status; 679 } 680 throw 'Failed to fetch results data.'; 681 } 682 return r.json(); 683 }, 684 err => err === 422, 685 5, 686 5000 687 ).then( 688 json => { 689 this.searchResults = json.results.sort((a, b) => a.test.localeCompare(b.test)); 690 this.refreshDisplayedNodes(); 691 }, 692 (e) => { 693 toast.close(); 694 // eslint-disable-next-line no-console 695 console.log(`Failed to load: ${e}`); 696 this.resultsLoadFailed = true; 697 } 698 ) 699 ); 700 } 701 702 fetchDiff() { 703 this.load( 704 window.fetch(this.diffURL) 705 .then(r => { 706 if (!r.ok || r.status !== 200) { 707 return Promise.reject('Failed to fetch diff data.'); 708 } 709 return r.json(); 710 }) 711 .then(json => { 712 this.diffResults = json; 713 this.refreshDisplayedNodes(); 714 }) 715 ); 716 } 717 718 pathUpdated(path) { 719 this.refreshDisplayedNodes(); 720 if (this.testRuns) { 721 this.sortCol = new Array(this.testRuns.length).fill(false); 722 this.isPathSorted = false; 723 } 724 this.showHistory = false 725 } 726 727 aggregateTestTotals(nodes, row, rs, diffRun) { 728 // Aggregation is done by test aggregation and subtest aggregation. 729 const aggregateTotalsBySubtest = (rs, i, diffRun) => { 730 const status = rs[i].status; 731 let passes = rs[i].passes; 732 let total = rs[i].total; 733 if (status) { 734 // Increment 'OK' status totals specifically for diff views. 735 // Diff views will still take harness status into account. 736 if (diffRun) { 737 total++; 738 if (status === 'O') passes++; 739 } else if (rs[i].total === 0) { 740 // If we're in subtest view and we have a test with no subtests, 741 // we should NOT ignore the test status and add it to the subtest c
741ount. 742 total++; 743 if (status === 'P') passes++; 744 } 745 } 746 return [passes, total]; 747 }; 748 749 const aggregateTotalsByTest = (rs, i) => { 750 const passingStatus = PASSING_STATUSES.includes(rs[i].status); 751 let passes = 0; 752 // If this is an old summary, aggregate using the old process. 753 if (!rs[i].newAggProcess) { 754 // Ignore aggregating test if there are no results. 755 if (rs[i].total === 0) { 756 return [0, 0]; 757 } 758 // Take the passes / total subtests to get a percentage passing. 759 passes = rs[i].passes / rs[i].total; 760 // If we have a total of 0 subtests but the status is passing, 761 // mark as 100% passing. 762 } else if (passingStatus && rs[i].total === 0) { 763 passes = 1; 764 // Otherwise, the passing percentage is the number of passes divided by the total. 765 } else if (rs[i].total > 0) { 766 passes = rs[i].passes / rs[i].total; 767 } 768 769 return [passes, 1]; 770 }; 771 772 for (let i = 0; i < rs.length; i++) { 773 const status = rs[i].status; 774 const isMissing = isTestMissing(rs[i].total, status); 775 row.results[i].singleSubtest = (rs[i].total === 0 && status && status !== 'O') || isMissing; 776 row.results[i].status = status; 777 let passes, total = 0; 778 [passes, total] = aggregateTotalsByTest(rs, i); 779 // Add the results to the total count of tests. 780 row.results[i].passes += passes; 781 nodes.totals[i].passes += passes; 782 row.results[i].total += total; 783 nodes.totals[i].total+= total; 784 785 [passes, total] = aggregateTotalsBySubtest(rs, i, diffRun); 786 // Initialize subtest counts to zero if not started. 787 if (!('subtest_total' in row.results[i])) { 788 row.results[i].subtest_passes = 0; 789 row.results[i].subtest_total = 0; 790 row.results[i].test_view_passes = 0; 791 row.results[i].test_view_total = 0; 792 } 793 row.results[i].subtest_passes += passes; 794 nodes.totals[i].subtest_passes += passes; 795 row.results[i].subtest_total += total; 796 nodes.totals[i].subtest_total += total; 797 const test_view_pass = isViewTestPass(total, passes, status) ? 1: 0; 798 row.results[i].test_view_passes += test_view_pass; 799 nodes.totals[i].test_view_passes += test_view_pass; 800 row.results[i].test_view_total++; 801 nodes.totals[i].test_view_total++; 802 } 803 } 804 805 refreshDisplayedNodes() { 806 if (!this.searchResults || !this.searchResults.length) { 807 this.displayedNodes = []; 808 return; 809 } 810 // Prefix: includes trailing slash. 811 const prefix = this.path === '/' ? '/' : `${this.path}/`; 812 const collapsePathOnto = (testPath, nodes) => { 813 const suffix = testPath.substring(prefix.length); 814 const slashIdx = suffix.split('?')[0].indexOf('/'); 815 const isDir = slashIdx !== -1; 816 const name = isDir ? suffix.substring(0, slashIdx) : suffix; 817 // Either add new node to acc, or add passes, total to an 818 // existing node. 819 if (!nodes.hasOwnProperty(name)) { 820 nodes[name] = { 821 path: `${prefix}${name}`, 822 isDir, 823 results: this.testRuns.map(() => ({ 824 passes: 0, 825 total: 0, 826 })), 827 }; 828 } 829 return name; 830 }; 831 832 const aggregateTestTotals = this.aggregateTestTotals; 833 const diffRun = this.diffRun 834 835 const resultsByPath = this.searchResults 836 // Filter out files not in this directory. 837 .filter(r => r.test.startsWith(prefix)) 838 // Accumulate displayedNodes from remaining files. 839 .reduce((nodes, r) => { 840 // Compute dir/file name that is direct descendant of this.path. 841 let testPath = r.test; 842 let previousTestPath; 843 if (this.diffResults && this.diffResults.renames) { 844 if (testPath in this.diffResults.renames) { 845 // This path was renamed; ignore. 846 return nodes; 847 } 848 const rename = Object.entries(this.diffResults.renames).find(e => e[1] === testPath); 849 if (rename) { 850 // This is the new path name; store the old one. 851 previousTestPath = rename[0]; 852 } 853 } 854 const name = collapsePathOnto(testPath, nodes); 855 856 const rs = r.legacy_status;
857 const row = nodes[name]; 858 if (!rs) { 859 return nodes; 860 } 861 862 // Keep track of overall total. 863 if (!('totals' in nodes)) { 864 nodes['totals'] = this.testRuns.map(() => { 865 return { passes: 0, total: 0, subtest_passes: 0, subtest_total: 0, test_view_passes: 0, test_view_total: 0 }; 866 }); 867 } 868 // Accumulate the sums. 869 aggregateTestTotals(nodes, row, r.legacy_status, diffRun); 870 871 if (previousTestPath) { 872 const previous = this.searchResults.find(r => r.test === previousTestPath); 873 if (previous) { 874 row.results[0].subtest_passes += previous.legacy_status[0].passes; 875 row.results[0].subtest_total += previous.legacy_status[0].total; 876 } 877 } 878 if (this.diff && rs.length === 2) { 879 let diff; 880 if (this.diffResults) { 881 diff = this.diffResults.diff[r.test]; 882 } else if (r.diff) { 883 diff = r.diff; 884 } else { 885 const [before, after] = rs; 886 diff = this.computeDifferences(before, after); 887 } 888 if (diff) { 889 row.diff = row.diff || { 890 passes: 0, 891 regressions: 0, 892 total: 0, 893 }; 894 row.diff.passes += diff[0]; 895 row.diff.regressions += diff[1]; 896 row.diff.total += diff[2]; 897 } 898 } 899 return nodes; 900 }, {}); 901 902 // Take the calculated totals to be displayed at bottom of results page. 903 // Delete key after reassignment. 904 this.displayedTotals = resultsByPath.totals; 905 delete resultsByPath.totals; 906 907 this.displayedNodes = Object.values(resultsByPath) 908 .filter(row => { 909 if (!this.onlyShowDifferences) { 910 return true; 911 } 912 return row.diff; 913 }); 914 } 915 916 computeDifferences(before, after) { 917 // Count statuses for diff views. 918 let beforePasses = before.passes; 919 let beforeTotal = before.total; 920 if (before.status) { 921 beforeTotal++; 922 if (PASSING_STATUSES.includes(before.status)) beforePasses++; 923 } 924 let afterPasses = after.passes; 925 let afterTotal = after.total; 926 if (after.status) { 927 afterTotal++; 928 if (PASSING_STATUSES.includes(after.status)) afterPasses++; 929 } 930 931 const deleted = beforeTotal > 0 && afterTotal === 0; 932 const added = afterTotal > 0 && beforeTotal === 0; 933 if (deleted && !this.diffFilter.includes('D') 934 || added && !this.diffFilter.includes('A')) { 935 return; 936 } 937 const failingBefore = beforeTotal - beforePasses; 938 const failingAfter = afterTotal - afterPasses; 939 const diff = [
940 Math.max(afterPasses - beforePasses, 0), // passes 941 Math.max(failingAfter - failingBefore, 0), // regressions 942 afterTotal - beforeTotal // total 943 ]; 944 const hasChanges = diff.some(v => v !== 0); 945 if ((this.diffFilter.includes('A') && added) 946 || (this.diffFilter.includes('D') && deleted) 947 || (this.diffFilter.includes('C') && hasChanges) 948 || (this.diffFilter.includes('U') && !hasChanges)) { 949 return diff; 950 } 951 } 952 953 platformID({ browser_name, browser_version, os_name, os_version }) { 954 return `${browser_name}-${browser_version}-${os_name}-${os_version}`; 955 } 956 957 canAmendMetadata(node, index, testRun) { 958 // It is always possible in triage mode to amend metadata for a problem 959 // with a test file itself. 960 if (index === undefined) { 961 return !node.isDir && this.isTriageMode; 962 } 963 964 // Triage can occur if a status doesn't pass. 965 const status = this.getNodeResultDataByPropertyName(node, index, testRun, 'status'); 966 const failStatus = status && !PASSING_STATUSES.includes(status); 967 const totalTests = this.getNodeResultDataByPropertyName(node, index, testRun, 'total'); 968 const passedTests = this.getNodeResultDataByPropertyName(node, index, testRun, 'passes'); 969 return ((totalTests - passedTests) > 0 || failStatus) && this.isTriageMode; 970 } 971 972 testResultClass(node, index, testRun, prop) { 973 // Guard against incomplete data. 974 if (!node || !testRun) { 975 return 'none'; 976 } 977 978 const result = node.results[index]; 979 const isDiff = this.isDiff(testRun); 980 if (isDiff) { 981 if (!node.diff) { 982 return 'none'; 983 } 984 // Diff case: 'delta [positive|negative|<nothing>]' based on delta 985 // value; 986 const delta = this.getDiffDelta(node, prop); 987 if (delta === 0) { 988 return 'delta'; 989 } 990 991 return `delta ${delta > 0 ? 'positive' : 'negative'}`; 992 } else { 993 // Change prop by view. 994 let prefix = ''; 995 if (this.isDefaultView()) { 996 prefix = 'subtest_' 997 } else if (this.isTestView()) { 998 prefix = 'test_view_'; 999 } 1000 // Non-diff case: result=undefined -> 'none'; path='/' -> 'top'; 1001 // result.passes=0 && result.total=0 -> 'top'; 1002 // otherwise -> 'passes-[colouring-by-percent]'. 1003 if (typeof result === 'undefined' && prop === 'total') { 1004 return 'none'; 1005 } 1006 // Percent view (interop-202*) will allow the home results to be colorized. 1007 if (this.path === '/' && !this.colorHomepage && !this.isInteropView()) { 1008 return 'top'; 1009 } 1010 if (result[`${prefix}passes`] === 0 && result[`${prefix}total`] === 0) { 1011 return 'top'; 1012 } 1013 return this.passRateClass(result[`${prefix}passes`], result[`${prefix}total`]); 1014 } 1015 } 1016 1017 shouldDisplayToggle(canViewInteropScores, pathIsATestFile) { 1018 return canViewInteropScores && !pathIsATestFile; 1019 } 1020 1021 interopButtonClass(view) { 1022 return (view === VIEW_ENUM.Interop) ? 'selected' : 'unselected'; 1023 } 1024 1025 defaultButtonClass(view) { 1026 return (view !== VIEW_ENUM.Interop && view !== VIEW_ENUM.Test) ? 'selected' : 'unselected'; 1027 } 1028 1029 clickInterop() { 1030 if (this.isInteropView()) { 1031 return; 1032 } 1033 this.view = VIEW_ENUM.Interop; 1034 } 1035 1036 clickDefault() { 1037 if (this.isDefaultView()) { 1038 return; 1039 } 1040 this.view = VIEW_ENUM.Subtest; 1041 } 1042 1043 changeView(view) { 1044 if (!view) { 1045 return; 1046 } 1047 // Change query string to display correct view. 1048 let query = location.search; 1049 if (query.length > 0) { 1050 query = query.substring(1) 1051 } 1052 let viewStr = `view=${view}`; 1053 const params = query.split('&'); 1054 let viewFound = false; 1055 for(let i = 0; i < params.length; i++) { 1056 if (params[i].includes('view=')) { 1057 viewFound = true; 1058 params[i] = viewStr; 1059 } 1060 } 1061 if (!viewFound) { 1062 params.push(viewStr) 1063 } 1064 1065 let url = location.pathname; 1066 url += `?${params.join('&')}`; 1067 history.pushState('', '', url) 1068 } 1069 1070 isDefaultView() { 1071 // Checks if a special view is active. 1072 return !this.isInteropView() && !this.isTestView(); 1073 } 1074 1075 isInteropView() { 1076 return this.view === VIEW_ENUM.Interop; 1077 } 1078 1079 isTestView() { 1080 return this.view === VIEW_ENUM.Test; 1081 } 1082 1083 getTotalsClass(totalInfo) { 1084 if ((this.path === '/' && !this.colorHomepage && this.isDefaultView()) 1085 || totalInfo.subtest_total === 0) { 1086 return 'top'; 1087 } 1088 if (this.isTestView()) { 1089 return this.passRateClass(totalInfo.test_view_passes, totalInfo.test_view_total); 1090 } 1091 if (!this.isDefaultView()) { 1092 return this.passRateClass(totalInfo.passes, totalInfo.total); 1093 } 1094 return this.passRateClass(totalInfo.subtest_passes, totalInfo.subtest_total); 1095 } 1096 1097 getDiffDelta(node, prop) { 1098 let val = 0; 1099 if (!prop) { 1100 val = Object.values(node.diff).forEach(v => val += Math.abs(v)); 1101 } else { 1102 val = node.diff[prop]; 1103 } 1104 return prop === 'regressions' ? -val : val; 1105 } 1106 1107 getDiffDeltaStr(node, prop) { 1108 const delta = this.getDiffDelta(node, prop); 1109 if (delta === 0) { 1110 return '0'; 1111 } 1112 const posOrNeg = delta > 0 ? '+' : ''; 1113 return `${posOrNeg}${delta}`;
1114 } 1115 1116 hasResults(node, testRun) { 1117 return typeof node.results[testRun.results_url] !== 'undefined'; 1118 } 1119 1120 isDiff(testRun) { 1121 return testRun && testRun.revision === 'diff'; 1122 } 1123 1124 getNodeResultDataByPropertyName(node, index, testRun, property) { 1125 if (this.isDiff(testRun)) { 1126 return this.getDiffDeltaStr(node, property); 1127 } 1128 if (index >= 0 && index < node.results.length) { 1129 return node.results[index][property]; 1130 } 1131 } 1132 1133 shouldDisplayHarnessWarning(node, index) { 1134 // Determine if a warning sign should be displayed next to subtest counts. 1135 const status = node.results[index].status; 1136 return !node.isDir && status && !PASSING_STATUSES.includes(status) 1137 && !node.results.every(testInfo => testInfo.singleSubtest); 1138 } 1139 1140 getStatusDisplay(node, index) { 1141 let status = node.results[index].status; 1142 if (status in STATUS_ABBREVIATIONS) { 1143 status = STATUS_ABBREVIATIONS[status]; 1144 } 1145 return status; 1146 } 1147 1148 // Formats the numbers shown on the results page for test aggregation. 1149 getTestNumbersDisplay(passes, total, isDir=true) { 1150 const formatPasses = parseFloat(passes.toFixed(2)); 1151 let cellDisplay = ''; 1152 1153 // To differentiate subtests from tests, a different separator is used. 1154 let separator = ' / '; 1155 if (!isDir) { 1156 separator = ' of '; 1157 } 1158 1159 // Show flat '0 / total' or 'total / total' only if none or all tests/subtests pass. 1160 // Display in parentheses if representing subtests. 1161 if (passes === 0) { 1162 cellDisplay = `0${separator}${total}`; 1163 } else if (passes === total) { 1164 cellDisplay = `${total}${separator}${total}`; 1165 } else if (formatPasses < 0.01) { 1166 // If there are passing tests, but only enough to round to 0.00, 1167 // show 0.01 rather than 0.00 to differentiate between possible error states. 1168 cellDisplay = `0.01${separator}${total}`; 1169 } else if (formatPasses === parseFloat(total)) { 1170 // If almost every test is passing, but there are some failures, 1171 // don't round up to 'total / total' so that it's clear some failure exists. 1172 cellDisplay = `${formatPasses - 0.01}`; 1173 } else { 1174 cellDisplay = `${formatPasses}${separator}${total}`; 1175 } 1176 return `${cellDisplay}`; 1177 } 1178 1179 // Formats the numbers shown on the results page for the interop view. 1180 formatCellDisplayInterop(passes, total, isDir) { 1181 1182 // Just show subtest numbers if we're at a single test view. 1183 if (!isDir) { 1184 return `${this.getTestNumbersDisplay(passes, total, isDir)} subtests`; 1185 } 1186 1187 const formatPercent = parseFloat((Math.floor(passes / total * 1000)) / 10); 1188 let cellDisplay = ''; 1189 // Show flat 0% or 100% only if none or all tests/subtests pass. 1190 if (passes === 0) { 1191 cellDisplay = '0'; 1192 } else if (passes === total) { 1193 cellDisplay = '100'; 1194 } else if (formatPercent === 0.0) { 1195 // If there are passing tests, but only enough to round to 0.00, 1196 // show 0.01 rather than 0.00 to differentiate between possible error states. 1197 cellDisplay = '0.1'; 1198 } else if (formatPercent === 100.0) { 1199 // If almost every test is passing, but there are some failures, 1200 // don't round up to 'total / total' so that it's clear some failure exists. 1201 cellDisplay = '99.9'; 1202 } else { 1203 cellDisplay = `${formatPercent}`; 1204 } 1205 return `${this.getTestNumbersDisplay(passes, total, isDir)} (${cellDisplay}%)`; 1206 } 1207 1208 // Formats the numbers shown on the results page for the test view. 1209 formatCellDisplayTestView(passes, total, status, isDir) { 1210 1211 // At the test level: 1212 // 1. Show PASS is passes == total for subtests AND (status is undefined or empty string (legacy) OR isPassingStatus (v2)). 1213 // 2. Show FAIL if status is undefined (legacy summaries) or 'O' (because showing OK would be misleading). 1214 // 3. Show FAIL otherwise. 1215 if (!isDir) { 1216 if (isViewTestPass(total, passes, status)) { 1217 return "PASS" 1218 } else if ((status === undefined) || (status === 'O')) { 1219 return "FAIL"; 1220 } else if (status in STATUS_ABBREVIATIONS) { 1221 return STATUS_ABBREVIATIONS[status]; 1222 } else { 1223 return "FAIL"; 1224 } 1225 } 1226
1227 // Only display the the numbers without percentages. 1228 return `${this.getTestNumbersDisplay(passes, total, isDir)}`; 1229 } 1230 1231 // Formats the numbers that will be shown in each cell on the results page. 1232 formatCellDisplay(passes, total, status=undefined, isDir=true) { 1233 // Display 'Missing' text if there are no tests or subtests. 1234 if (total === 0 && !status) { 1235 return 'Missing'; 1236 } 1237 1238 // If the view is not the default view (subtest), then check for the 'interop' view. 1239 // If view is 'interop', use that format instead. 1240 if (this.isInteropView()) { 1241 return this.formatCellDisplayInterop(passes, total, isDir); 1242 } 1243 1244 // If the view is not the default view (subtest), then check for the 'test' view. 1245 // If view is 'test', use that format instead. 1246 if (this.isTestView()) { 1247 return this.formatCellDisplayTestView(passes, total, status, isDir); 1248 } 1249 1250 // If we're in the subtest view and there are no subtests but a status exists, 1251 // we should count the status as the test total. 1252 if (total === 0) { 1253 if (status === 'P') return `${passes + 1} / ${total + 1}`; 1254 return `${passes} / ${total + 1}`; 1255 } 1256 return `${passes} / ${total}`; 1257 } 1258 1259 isSubtestView(node) { 1260 return this.isDefaultView() || !node.isDir; 1261 } 1262 1263 getNodeTotalProp(node) { 1264 if (this.isTestView()) { 1265 return 'test_view_total'; 1266 } 1267 // Display test numbers at directory level, but subtest numbers when showing a single test. 1268 return this.isSubtestView(node) ? 'subtest_total': 'total'; 1269 } 1270 1271 getNodePassProp(node) { 1272 if (this.isTestView()) { 1273 return 'test_view_passes'; 1274 } 1275 // Display test numbers at directory level, but subtest numbers when showing a single test. 1276 return this.isSubtestView(node) ? 'subtest_passes': 'passes'; 1277 } 1278 1279 getNodeResult(node, index) { 1280 const status = node.results[index].status; 1281 const passesProp = this.getNodePassProp(node); 1282 const totalProp = this.getNodeTotalProp(node); 1283 // Calculate what should be displayed in a given results row. 1284 let passes = node.results[index][passesProp]; 1285 let total = node.results[index][totalProp]; 1286 return this.formatCellDisplay(passes, total, status, node.isDir); 1287 } 1288 1289 // Format and display the information shown in the totals cells. 1290 getTotalDisplay(totalInfo) { 1291 let passes = totalInfo.subtest_passes; 1292 let total = totalInfo.subtest_total; 1293 if (this.isInteropView()) { 1294 passes = totalInfo.passes; 1295 total = totalInfo.total; 1296 } 1297 if (this.isTestView()) { 1298 passes = totalInfo.test_view_passes; 1299 total = totalInfo.test_view_total; 1300 } 1301 return this.formatCellDisplay(passes, total); 1302 } 1303 1304 getTotalText() { 1305 if (this.isDefaultView()) { 1306 return 'Subtest Total'; 1307 } 1308 return 'Test Total'; 1309 } 1310 1311 /* Function for getting total numbers. 1312 * Intentionally not exposed in UI. 1313 * To generate, open your console and run: 1314 * document.querySelector('wpt-results').generateTotalPassNumbers() 1315 */ 1316 generateTotalPassNumbers() { 1317 const totals = {}; 1318 1319 this.testRuns.forEach(testRun => { 1320 const testRunID = this.platformID(testRun); 1321 totals[testRunID] = { passes: 0, total: 0 }; 1322 1323 Object.keys(this.specDirs).forEach(specKey => { 1324 let { passes, total } = this.specDirs[specKey].results[testRun.results_url]; 1325 1326 totals[testRunID].passes += passes; 1327 totals[testRunID].total += total; 1328 }); 1329 }); 1330 1331 Object.keys(totals).forEach(key => { 1332 totals[key].percent = (totals[key].passes / totals[key].total) * 100; 1333 }); 1334 1335 // eslint-disable-next-line no-console 1336 console.table(Object.keys(totals).map(k => ({ 1337 platformID: k, 1338 passes: totals[k].passes, 1339 total: totals[k].total, 1340 percent: totals[k].percent 1341 }))); 1342 1343 // eslint-disable-next-line no-console 1344 console.log('JSON version:', JSON.stringify(totals)); 1345 } 1346 1347 showHistoryClicked() { 1348 return () => { 1349 this.showHistory = true; 1350 }; 1351 } 1352 1353 queryChanged(query, queryBefore) { 1354 super.queryChanged(query, queryBefore); 1355 // TODO (danielrsmith): fix the query logic so that this statement isn't needed 1356 // to avoid duplicate calls. Hacky fix here that will not reload the data if 1357 // 'view' is the only query string param. 1358 if (query.includes('view') && query.split('=').length === 2) { 1359 return; 1360 } 1361 1362 if (this._fetchedQuery === query) { 1363 return; 1364 } 1365 this._fetchedQuery = query; // Debounce. 1366 this.reloadData(); 1367 } 1368 1369 moveToNext() { 1370 this._move(true); 1371 } 1372 1373 moveToPrev() { 1374 this._move(false); 1375 } 1376 1377 _move(forward) { 1378 if (!this.searchResults || !this.searchResults.length) { 1379 return; 1380 } 1381 const n = this.searchResults.length; 1382 let next = this.searchResults.findIndex(r => r.test.startsWith(this.path)); 1383 if (next < 0) { 1384 next = (forward ? 0 : -1); 1385 } else if (this.searchResults[next].test === this.path) { // Only advance 1 for exact match. 1386 next = next + (forward ? 1 : -1); 1387 } 1388 // % in js is not modulo, it's remainder. Ensure it's positive. 1389 this.path = this.searchResults[(n + next) % n].test; 1390 } 1391 1392 sortTestName() { 1393 if (!this.displayedNodes) { 1394 return; 1395 } 1396 1397 this.isPathSorted = !this.isPathSorted; 1398 this.sortCol = new Array(this.testRuns.length).fill(false); 1399 const sortedNodes = this.displayedNodes.slice(); 1400 sortedNodes.sort((a, b) => { 1401 if (this.isPathSorted) { 1402 return this.compareTestNameDefaultOrder(a, b); 1403 } 1404 return this.compareTestNameDefaultOrder(b, a); 1405 }); 1406 this.displayedNodes = sortedNodes; 1407 } 1408 1409 compareTestName(a, b) { 1410 if (this.isPathSorted) { 1411 return this.compareTestNameDefaultOrder(a, b); 1412 } 1413 return this.compareTestNameDefaultOrder(b, a); 1414 } 1415 1416 compareTestNameDefaultOrder(a, b) { 1417 const pathA = a.path.toLowerCase(); 1418 const pathB = b.path.toLowerCase(); 1419 if (pathA < pathB) { 1420 return -1; 1421 } 1422 1423 if (pathA > pathB) { 1424 return 1; 1425 } 1426 return 0; 1427 } 1428 1429 sortTestResults(index) { 1430 return () => { 1431 if (!this.displayedNodes) { 1432 return; 1433 } 1434 1435 const sortedNodes = this.displayedNodes.slice(); 1436 sortedNodes.sort((a, b) => { 1437 if (this.sortCol[index]) { 1438 // Switch a and b to reverse the order; 1439 const c = a; 1440 a = b; 1441 b = c; 1442 } 1443 // Use numbers based on view. 1444 let passesParam = 'passes'; 1445 let totalParam = 'total'; 1446 if (this.isDefaultView()) { 1447 passesParam = 'subtest_passes'; 1448 totalParam = 'subtest_total'; 1449 } else if (this.isTestView()) { 1450 passesParam = 'test_view_passes'; 1451 totalParam = 'test_view_total'; 1452 } 1453 1454 // Both 0/0 cases; compare test names. 1455 if (a.results[index][totalParam] === 0 && b.results[index][totalParam] === 0) { 1456 return this.compareTestNameDefaultOrder(a, b); 1457 } 1458 1459 // One of them is 0/0; compare passes; 1460 if (a.results[index][totalParam] === 0 || b.results[index][totalParam] === 0) { 1461 return a.results[index][totalParam] - b.results[index][totalParam]; 1462 } 1463 const percentageA = a.results[index][passesParam] / a.results[index][totalParam]; 1464 const percentageB = b.results[index][passesParam] / b.results[index][totalParam]; 1465 if (percentageA === percentageB) { 1466 return this.compareTestNameDefaultOrder(a, b); 1467 } 1468 return percentageA - percentageB; 1469 }); 1470 1471 const newSortCol = new Array(this.sortCol.length).fill(false); 1472 newSortCol[index] = !this.sortCol[index]; 1473 this.sortCol = newSortCol; 1474 this.isPathSorted = false;
1475 this.displayedNodes = sortedNodes; 1476 }; 1477 } 1478 1479 getSortIcon(isSorted) { 1480 if (isSorted) { 1481 return '/static/expand_more.svg'; 1482 } 1483 return '/static/expand_less.svg'; 1484 } 1485 1486 handleTriageMode(isTriageMode) { 1487 if (isTriageMode && this.pathIsATestFile) { 1488 return; 1489 } 1490 this.handleTriageModeChange(isTriageMode, this.$['selected-toast']); 1491 } 1492 1493 clearSelectedCells() { 1494 if (this.selectedMetadata.length === 0) { 1495 this.handleClear(this.selectedMetadata); 1496 } 1497 } 1498 1499 selectedMetadataChanged(e) { 1500 this.selectedMetadata = e.detail.value; 1501 } 1502 1503 metadataMapChanged(e) { 1504 this.metadataMap = e.detail.value; 1505 } 1506 1507 labelMapChanged(e) { 1508 this.labelMap = e.detail.value; 1509 } 1510 1511 handleTriageHover() { 1512 const [index, node, testRun] = arguments; 1513 return (e) => { 1514 this.handleHover(e.target.closest('td'), this.canAmendMetadata(node, index, testRun)); 1515 }; 1516 } 1517 1518 handleTriageSelect() { 1519 const [index, node, testRun] = arguments; 1520 return (e) => { 1521 if (!this.canAmendMetadata(node, index, testRun)) { 1522 return; 1523 } 1524 1525 const product = index === undefined ? '' : this.displayedProducts[index].browser_name; 1526 this.handleSelect(e.target.closest('td'), product, node.path, this.$['selected-toast']); 1527 }; 1528 } 1529 1530 handleReloadPendingMetadata() { 1531 this.triageNotifier = !this.triageNotifier; 1532 } 1533 1534 openAmendMetadata() { 1535 this.$.amend.open(); 1536 } 1537 1538 shouldDisplayTestLabel(testname, labelMap) { 1539 return this.getTestLabel(testname, labelMap) !== ''; 1540 } 1541 1542 shouldDisplayTotals(displayedTotals, diffRun) { 1543 return !diffRun && displayedTotals && displayedTotals.length > 0; 1544 } 1545 1546 getTestLabelTitle(testname, labelMap) { 1547 const labels = this.getTestLabel(testname, labelMap); 1548 if (labels.includes(',')) { 1549 return 'labels: ' + labels; 1550 } 1551 return 'label: ' + labels; 1552 } 1553 1554 getTestLabel(testname, labelMap) { 1555 if (!labelMap) { 1556 return ''; 1557 } 1558 1559 if (this.computePathIsASubfolder(testname)) { 1560 testname = testname + '/*'; 1561 } 1562 1563 if (testname in labelMap) { 1564 return labelMap[testname]; 1565 } 1566 1567 return ''; 1568 } 1569 1570 shouldDisplayMetadata(index, testname, metadataMap) { 1571 return !this.pathIsRootDir && this.getMetadataUrl(index, testname, metadataMap) !== ''; 1572 } 1573 1574 getMetadataUrl(index, testname, metadataMap) { 1575 if (!metadataMap) { 1576 return ''; 1577 } 1578 1579 if (this.computePathIsASubfolder(testname)) { 1580 testname = testname + '/*'; 1581 } 1582 1583 const browserName = index === undefined ? '' : this.displayedProducts[index].browser_name; 1584 const key = testname + browserName; 1585 if (key in metadataMap) { 1586 if ('/' in metadataMap[key]) { 1587 return metadataMap[key]['/']; 1588 } 1589 1590 // If a URL link does not exist on a test level, return the first subtest link. 1591 const subtestMap = metadataMap[key]; 1592 return subtestMap[Object.keys(subtestMap)[0]]; 1593 } 1594 return ''; 1595 } 1596} 1597 1598window.customElements.define(WPTResults.is, WPTResults); 1599 1600export { WPTResults };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.