github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/views/wpt-app.js (about) 1 import { PathInfo } from '../components/path.js'; 2 import '../components/test-runs-query-builder.js'; 3 import { TestRunsUIBase } from '../components/test-runs.js'; 4 import '../components/test-search.js'; 5 import '../components/wpt-flags.js'; 6 import { WPTFlags } from '../components/wpt-flags.js'; 7 import '../components/wpt-header.js'; 8 import '../components/wpt-permalinks.js'; 9 import '../components/wpt-bsf.js'; 10 import '../node_modules/@polymer/app-route/app-location.js'; 11 import '../node_modules/@polymer/app-route/app-route.js'; 12 import '../node_modules/@polymer/iron-collapse/iron-collapse.js'; 13 import '../node_modules/@polymer/iron-pages/iron-pages.js'; 14 import '../node_modules/@polymer/paper-icon-button/paper-icon-button.js'; 15 import '../node_modules/@polymer/polymer/lib/elements/dom-if.js'; 16 import { html } from '../node_modules/@polymer/polymer/polymer-element.js'; 17 import '../views/wpt-404.js'; 18 import '../views/wpt-results.js'; 19 20 class WPTApp extends PathInfo(WPTFlags(TestRunsUIBase)) { 21 static get is() { 22 return 'wpt-app'; 23 } 24 25 static get template() { 26 return html` 27 <style> 28 section.search { 29 position: relative; 30 } 31 section.search .path { 32 margin-top: 1em; 33 } 34 section.search paper-spinner-lite { 35 position: absolute; 36 top: 0; 37 right: 0; 38 } 39 a { 40 color: #0d5de6; 41 text-decoration: none; 42 } 43 .separator { 44 border-bottom: solid 1px var(--paper-grey-300); 45 padding-bottom: 1em; 46 margin-bottom: 1em; 47 } 48 .path { 49 margin-bottom: 16px; 50 } 51 .path-separator { 52 padding: 0 0.1em; 53 margin: 0 0.2em; 54 } 55 .links { 56 margin-bottom: 1em; 57 } 58 test-runs-query-builder { 59 display: block; 60 margin-bottom: 32px; 61 } 62 .query-actions paper-button { 63 display: inline-block; 64 } 65 paper-icon-button { 66 vertical-align: middle; 67 margin-right: 10px; 68 padding: 0px; 69 height: 28px; 70 } 71 </style> 72 73 <app-location route="{{route}}" url-space-regex="^/(results)/"></app-location> 74 <app-route route="{{route}}" pattern="/:page" data="{{routeData}}" tail="{{subroute}}"></app-route> 75 76 <wpt-header path="[[encodedPath]]" query="[[query]]" user="[[user]]" is-triage-mode="[[isTriageMode]]"></wpt-header> 77 78 <section class="search"> 79 <div class="path"> 80 <a href="/[[page]]/?[[ query ]]">wpt</a> 81 <!-- The next line is intentionally formatted so to avoid whitespaces between elements. --> 82 <template is="dom-repeat" items="[[ splitPathIntoLinkedParts(path) ]]" as="part" 83 ><span class="path-separator">/</span><a href="/[[page]][[ part.path ]]?[[ query ]]">[[ part.name ]]</a></template> 84 </div> 85 86 <paper-spinner-lite active="[[isLoading]]" class="blue"></paper-spinner-lite> 87 88 <test-search query="[[search]]" 89 structured-query="{{structuredSearch}}" 90 test-runs="[[testRuns]]" 91 test-paths="[[testPaths]]"> 92 </test-search> 93 94 <template is="dom-if" if="[[ pathIsATestFile ]]"> 95 <div class="links"> 96 <ul> 97 <li> 98 View source on GitHub 99 (<a href\$="https://github.com/web-platform-tests/wpt/blob/[[testRuns.0.revision]][[path]]" target="_blank">current commit</a>) 100 (<a href\$="https://github.com/web-platform-tests/wpt/blob/master[[path]]" target="_blank">master branch</a>) 101 </li> 102 103 <template is="dom-if" if="[[ !webPlatformTestsLive ]]"> 104 <li><a href\$="[[scheme]]://w3c-test.org[[path]]" target="_blank">Run in your 105 browser on w3c-test.org</a></li> 106 </template> 107 108 <template is="dom-if" if="[[ webPlatformTestsLive ]]"> 109 <li><a href\$="[[scheme]]://wpt.live[[path]]" target="_blank">Run in your 110 browser on wpt.live</a></li> 111 </template> 112 </ul> 113 </div> 114 </template> 115 </section> 116 117 <div class="separator"></div> 118 119 <template is="dom-if" if="[[showBSFGraph]]"> 120 <div onmouseenter="[[enterBSF]]" onmouseleave="[[exitBSF]]"> 121 <info-banner> 122 <paper-icon-button src="[[getCollapseIcon(isBSFCollapsed)]]" onclick="[[handleCollapse]]" aria-label="Hide BSF graph"></paper-icon-button> 123 [[bsfBannerMessage]] 124 </info-banner> 125 <template is="dom-if" if="[[!isBSFCollapsed]]"> 126 <iron-collapse opened="[[!isBSFCollapsed]]"> 127 <wpt-bsf is-interacting="[[isInteracting]]" on-interactingchanged="bsfIsInteractingChanged"></wpt-bsf> 128 </iron-collapse> 129 </template> 130 </div> 131 </template> 132 133 <template is="dom-if" if="[[resultsTotalsRangeMessage]]"> 134 <info-banner> 135 [[resultsTotalsRangeMessage]] 136 <template is="dom-if" if="[[!editable]]"> 137 <a href="javascript:window.location.search='';"> (switch to the default product set instead)</a> 138 </template> 139 <wpt-permalinks path="[[path]]" 140 path-prefix="/[[page]]/" 141 query-params="[[queryParams]]" 142 test-runs="[[testRuns]]"> 143 </wpt-permalinks> 144 <paper-button onclick="[[togglePermalinks]]" slot="small">Link</paper-button> 145 <paper-button onclick="[[toggleQueryEdit]]" slot="small" hidden="[[!editable]]">Edit</paper-button> 146 </info-banner> 147 </template> 148 <iron-collapse opened="[[editingQuery]]"> 149 <test-runs-query-builder query-params="[[queryParams]]" on-submit="[[submitQuery]]"></test-runs-query-builder> 150 </iron-collapse> 151 152 <iron-pages role="main" selected="[[page]]" attr-for-selected="name" selected-attribute="visible" fallback-selection="404"> 153 <wpt-results name="results" 154 is-loading="{{resultsLoading}}" 155 structured-search="[[structuredSearch]]" 156 path="[[subroute.path]]" 157 test-runs="[[testRuns]]" 158 test-paths="{{testPaths}}" 159 search-results="{{searchResults}}" 160 subtest-row-count={{subtestRowCount}} 161 is-triage-mode="[[isTriageMode]]" 162 on-testrunsload="handleTestRunsLoad" 163 view="[[view]]"></wpt-results> 164 165 <wpt-404 name="404" ></wpt-404> 166 </iron-pages> 167 168 <paper-toast id="masterLabelMissing" duration="15000"> 169 <div style="display: flex;"> 170 wpt.fyi now includes affected tests results from PRs. <br> 171 Did you intend to view results for complete (master) runs only? 172 <paper-button onclick="[[addMasterLabel]]">View master runs</paper-button> 173 <paper-button onclick="[[dismissToast]]">Dismiss</paper-button> 174 </div> 175 </paper-toast> 176 `; 177 } 178 179 static get properties() { 180 return { 181 page: { 182 type: String, 183 reflectToAttribute: true, 184 }, 185 user: String, 186 path: String, 187 testPaths: Set, 188 structuredSearch: Object, 189 resultsLoading: Boolean, 190 editable: { 191 type: Boolean, 192 computed: 'computeEditable(queryParams)', 193 }, 194 isLoading: { 195 type: Boolean, 196 computed: '_computeIsLoading(resultsLoading)', 197 }, 198 searchResults: Array, 199 resultsTotalsRangeMessage: { 200 type: String, 201 computed: 'computeResultsTotalsRangeMessage(page, path, searchResults, shas, productSpecs, to, from, maxCount, labels, master, runIds, subtestRowCount)', 202 }, 203 subtestRowCount: Number, 204 bsfBannerMessage: { 205 type: String, 206 computed: 'computeBSFBannerMessage(isBSFCollapsed)', 207 }, 208 showBSFGraph: { 209 type: Boolean, 210 computed: 'computeShowBSFGraph(page, queryParams, pathIsRootDir, showBSF)', 211 }, 212 isBSFCollapsed: { 213 type: Boolean, 214 computed: 'computeIsBSFCollapsed()', 215 }, 216 isTriageMode: { 217 type: Boolean, 218 value: false, 219 }, 220 bsfStartTime: { 221 type: Object, 222 value: null, 223 }, 224 isInteracting: Boolean, 225 }; 226 } 227 228 static get observers() { 229 return [ 230 '_routeChanged(routeData, routeData.*)', 231 '_subrouteChanged(subroute, subroute.*)', 232 ]; 233 } 234 235 constructor() { 236 super(); 237 this.togglePermalinks = () => this.shadowRoot.querySelector('wpt-permalinks').open(); 238 this.toggleQueryEdit = () => { 239 this.editingQuery = !this.editingQuery; 240 }; 241 this.handleCollapse = () => { 242 this.isBSFCollapsed = !this.isBSFCollapsed; 243 // Record hide/open actions on the BSF graph. Currently, we only 244 // show it on the homepage. 245 if ('gtag' in window) { 246 window.gtag('event', 'visibility change', { 247 'event_category': 'bsf', 248 'event_label': this.path, 249 'value': this.isBSFCollapsed ? 1 : 0 250 }); 251 } 252 this.setLocalStorageFlag(this.isBSFCollapsed, 'isBSFCollapsed'); 253 }; 254 this.enterBSF = () => { 255 // The use of isInteracting is a workaround for a known issue, 256 // https://stackoverflow.com/questions/17244996/why-do-the-mouseenter-mouseleave-events-fire-when-entering-leaving-child-element; 257 // when users interact with the BSF chart itself, enterBSF is triggered unexpectedly. 258 // In that case, isInteracting is set to true to avoid resetting bsfStartTime. 259 if (this.isInteracting) { 260 return; 261 } 262 this.bsfStartTime = new Date(); 263 }; 264 this.exitBSF = () => { 265 // Similarly, when users interact with the BSF chart, isInteracting is set to 266 // true to avoid sending analytics prematurely in exitBSF. 267 if (this.isInteracting || !this.bsfStartTime) { 268 return; 269 } 270 const diff = new Date().getTime() - this.bsfStartTime.getTime(); 271 const duration = Math.round(diff / 1000); 272 if (duration <= 0) { 273 return; 274 } 275 276 if ('gtag' in window) { 277 window.gtag('event', 'hover', { 278 'event_category': 'bsf', 279 'event_label': this.path, 280 'value': duration 281 }); 282 } 283 this.bsfStartTime = null; 284 }; 285 this.submitQuery = this.handleSubmitQuery.bind(this); 286 this.addMasterLabel = this.handleAddMasterLabel.bind(this); 287 this.dismissToast = e => e.target.closest('paper-toast').close(); 288 } 289 290 connectedCallback() { 291 super.connectedCallback(); 292 const testSearch = this.shadowRoot.querySelector('test-search'); 293 testSearch.addEventListener('commit', this.handleSearchCommit.bind(this)); 294 testSearch.addEventListener('autocomplete', this.handleSearchAutocomplete.bind(this)); 295 document.addEventListener('keydown', this.handleKeyDown.bind(this)); 296 this.addEventListener('triagemode', this.handleTriageToggle.bind(this)); 297 } 298 299 disconnectedCallback() { 300 const testSearch = this.shadowRoot.querySelector('test-search'); 301 testSearch.removeEventListener('commit', this.handleSearchCommit.bind(this)); 302 testSearch.removeEventListener('autocomplete', this.handleSearchAutocomplete.bind(this)); 303 super.disconnectedCallback(); 304 } 305 306 ready() { 307 super.ready(); 308 // Show warning about ?label=experimental missing the master label. 309 const labels = this.queryParams && this.queryParams.label; 310 if (labels && labels.includes('experimental') && !labels.includes('master')) { 311 this.shadowRoot.querySelector('#masterLabelMissing').show(); 312 } 313 this.shadowRoot.querySelector('app-location') 314 ._createPropertyObserver('__query', query => this.query = query); 315 this.addEventListener('interactingchanged', this.bsfIsInteractingChanged); 316 } 317 318 bsfIsInteractingChanged(e) { 319 this.isInteracting = e.detail.value; 320 } 321 322 queryChanged(query) { 323 // app-location don't support repeated params. 324 this.shadowRoot.querySelector('app-location').__query = query; 325 if (this.activeView) { 326 this.activeView.query = query; 327 } 328 super.queryChanged(query); 329 } 330 331 _routeChanged(routeData) { 332 this.page = routeData.page || 'results'; 333 if (this.activeView) { 334 this.activeView.query = this.query; 335 } 336 } 337 338 _subrouteChanged(subroute) { 339 this.path = subroute.path || '/'; 340 } 341 342 get activeView() { 343 return this.shadowRoot.querySelector(`wpt-${this.page}`); 344 } 345 346 _computeIsLoading(resultsLoading) { 347 return resultsLoading; 348 } 349 350 handleKeyDown(e) { 351 // Ignore when something other than body has focus. 352 if (e.target !== document.body) { 353 return; 354 } 355 if (e.key === 'n') { 356 this.activeView.moveToNext(); 357 } else if (e.key === 'p') { 358 this.activeView.moveToPrev(); 359 } 360 } 361 362 handleSubmitQuery() { 363 const builder = this.shadowRoot.querySelector('test-runs-query-builder'); 364 this.editingQuery = false; 365 this.updateQueryParams(builder.queryParams); 366 } 367 368 handleSearchCommit(e) { 369 const batchUpdate = { 370 search: e.detail.query, 371 structuredSearch: e.detail.structuredQuery, 372 }; 373 this.setProperties(batchUpdate); 374 } 375 376 handleSearchAutocomplete(e) { 377 this.shadowRoot.querySelector('test-search').clear(); 378 this.set('subroute.path', e.detail.path); 379 } 380 381 handleAddMasterLabel(e) { 382 const builder = this.shadowRoot.querySelector('test-runs-query-builder'); 383 builder.master = true; 384 this.handleSubmitQuery(); 385 this.dismissToast(e); 386 } 387 388 handleTriageToggle(e) { 389 this.isTriageMode = e.detail.val; 390 } 391 392 handleTestRunsLoad(e) { 393 this.testRuns = e.detail.testRuns; 394 } 395 396 computeEditable(queryParams) { 397 if (queryParams.run_id || 'max-count' in queryParams) { 398 return false; 399 } 400 return true; 401 } 402 403 computeResultsTotalsRangeMessage(page, path, searchResults, shas, productSpecs, from, to, maxCount, labels, master, runIds, subtestRowCount) { 404 const msg = super.computeResultsRangeMessage(shas, productSpecs, from, to, maxCount, labels, master, runIds); 405 if (page === 'results' && searchResults) { 406 // If the view is displaying subtests of a single test, 407 // we show the number of rows excluding Harness duration. 408 if (this.computePathIsATestFile(path)) { 409 if (!subtestRowCount || subtestRowCount === 1) { 410 return msg; 411 } 412 return msg.replace('Showing ', `Showing ${subtestRowCount} subtests from `); 413 } 414 let subtests = 0, tests = 0; 415 for (const r of searchResults) { 416 if (r.test.startsWith(this.path)) { 417 tests++; 418 subtests += Math.max(...r.legacy_status.map(s => s.total)); 419 } 420 } 421 let folder = ''; 422 if (path && path.length > 1) { 423 folder = ` in ${path.substring(1)}`; 424 } 425 let testsAndSubtests = ''; 426 if (tests > 1) { 427 testsAndSubtests += `${tests} tests`; 428 if (subtests > 1) { 429 testsAndSubtests += ` (${subtests} subtests)`; 430 } 431 testsAndSubtests += folder; 432 } 433 return msg.replace( 434 'Showing ', 435 `Showing ${testsAndSubtests} from `); 436 } 437 return msg; 438 } 439 440 computeBSFBannerMessage(isBSFCollapsed) { 441 const actionText = isBSFCollapsed ? 'expand' : 'collapse'; 442 return `Browser Specific Failures graph (click the arrow to ${actionText})`; 443 } 444 445 // Currently we only have BSF data for the entirety of the WPT test suite. To avoid 446 // confusing the user, we only display the graph when they are looking at top-level 447 // test results and hide it when in a subdirectory. 448 computeShowBSFGraph(page, queryParams, pathIsRootDir, showBSF) { 449 // Only show on the results page. 450 if (page !== 'results') { 451 return false; 452 } 453 454 // Hide when search is in use or query by run_id/sha. 455 if (queryParams.q || queryParams.run_id || queryParams.sha) { 456 return false; 457 } 458 459 return pathIsRootDir && showBSF; 460 } 461 462 computeIsBSFCollapsed() { 463 const stored = this.getLocalStorageFlag('isBSFCollapsed'); 464 if (stored === null) { 465 return false; 466 } 467 return stored; 468 } 469 470 getCollapseIcon(isBSFCollapsed) { 471 if (isBSFCollapsed) { 472 return '/static/expand_more.svg'; 473 } 474 return '/static/expand_less.svg'; 475 } 476 } 477 customElements.define(WPTApp.is, WPTApp); 478 479 export { WPTApp };