github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/wpt-metadata.js (about) 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 7 import '../node_modules/@polymer/iron-collapse/iron-collapse.js'; 8 import '../node_modules/@polymer/paper-button/paper-button.js'; 9 import '../node_modules/@polymer/polymer/lib/elements/dom-if.js'; 10 import '../node_modules/@polymer/polymer/lib/elements/dom-repeat.js'; 11 import { 12 html, 13 PolymerElement 14 } from '../node_modules/@polymer/polymer/polymer-element.js'; 15 import { LoadingState } from './loading-state.js'; 16 import { PathInfo } from '../components/path.js'; 17 import { ProductInfo } from './product-info.js'; 18 19 class WPTMetadataNode extends ProductInfo(PolymerElement) { 20 static get template() { 21 return html` 22 <style> 23 img.browser { 24 height: 16px; 25 width: 16px; 26 position: relative; 27 top: 2px; 28 } 29 img.bug { 30 margin-right: 16px; 31 height: 24px; 32 width: 24px; 33 } 34 .metadataNode { 35 display: flex; 36 align-items: center; 37 margin-bottom: 4px; 38 } 39 40 </style> 41 <div class="metadataNode"> 42 <iron-icon class="bug" icon="bug-report"></iron-icon> 43 <div> 44 <a href="[[testHref]]" target="_blank">[[metadataNode.test]]</a> > 45 <img class="browser" src="[[displayMetadataLogo(metadataNode.product)]]"> : 46 <a href="[[metadataNode.url]]" target="_blank">[[metadataNode.url]]</a> 47 <br /> 48 </div> 49 </div> 50 `; 51 } 52 53 static get is() { 54 return 'wpt-metadata-node'; 55 } 56 57 static get properties() { 58 return { 59 path: String, 60 metadataNode: Object, 61 testHref: { 62 type: String, 63 computed: 'computeTestHref(path, metadataNode)' 64 } 65 }; 66 } 67 68 computeTestHref(path, metadataNode) { 69 const currentUrl = window.location.href; 70 let testname = metadataNode.test; 71 if (testname.endsWith('/*')) { 72 return currentUrl.replace(path, testname.substring(0, testname.length - 2)); 73 } 74 return currentUrl.replace(path, testname); 75 } 76 } 77 window.customElements.define(WPTMetadataNode.is, WPTMetadataNode); 78 79 class WPTMetadata extends PathInfo(LoadingState(PolymerElement)) { 80 static get template() { 81 return html` 82 <style> 83 h4 { 84 margin-bottom: 0.5em; 85 } 86 </style> 87 <template is="dom-if" if="[[!pathIsRootDir]]"> 88 <template is="dom-if" if="[[firstThree]]"> 89 <h4>Relevant links for <i>[[path]]</i> results</h4> 90 </template> 91 <template is="dom-repeat" items="[[firstThree]]" as="metadataNode"> 92 <wpt-metadata-node metadata-node="[[metadataNode]]" path="[[path]]"></wpt-metadata-node> 93 </template> 94 <template is="dom-if" if="[[others]]"> 95 <iron-collapse id="metadata-collapsible"> 96 <template is="dom-repeat" items="[[others]]" as="metadataNode"> 97 <wpt-metadata-node 98 metadata-node="[[metadataNode]]" 99 path="[[path]]" 100 ></wpt-metadata-node> 101 </template> 102 </iron-collapse> 103 <paper-button id="metadata-toggle" onclick="[[openCollapsible]]"> 104 Show more 105 </paper-button> 106 </template> 107 <br> 108 </template> 109 `; 110 } 111 112 static get is() { 113 return 'wpt-metadata'; 114 } 115 116 static get properties() { 117 return { 118 products: { 119 type: Array, 120 observer: 'loadMergedMetadata' 121 }, 122 searchResults: Array, 123 testResultSet: { 124 type: Object, 125 computed: 'computeTestResultSet(searchResults)', 126 }, 127 path: String, 128 // metadata maps test => links 129 metadata: { 130 type: Object, 131 computed: 'computeMetadata(mergedMetadata, pendingMetadata)', 132 }, 133 mergedMetadata: Object, 134 pendingMetadata: Object, 135 displayedMetadata: { 136 type: Array, 137 computed: 'computeDisplayedMetadata(path, metadata, testResultSet)', 138 }, 139 firstThree: { 140 type: Array, 141 computed: 'computeFirstThree(displayedMetadata)' 142 }, 143 others: { 144 type: Array, 145 computed: 'computeOthers(displayedMetadata)' 146 }, 147 metadataMap: { 148 type: Object, 149 notify: true, 150 }, 151 labelMap: { 152 type: Object, 153 notify: true, 154 }, 155 triageNotifier: Boolean, 156 }; 157 } 158 159 static get observers() { 160 return [ 161 'loadPendingMetadata(triageNotifier)', 162 ]; 163 } 164 165 constructor() { 166 super(); 167 this.loadPendingMetadata(); 168 this.openCollapsible = this.handleOpenCollapsible.bind(this); 169 } 170 171 _resetSelectors() { 172 const button = this.shadowRoot.querySelector('#metadata-toggle'); 173 const collapse = this.shadowRoot.querySelector('#metadata-collapsible'); 174 if (this.others && button && collapse) { 175 button.hidden = false; 176 collapse.opened = false; 177 } 178 } 179 180 // loadMergedMetadata is called when products is changed. 181 loadMergedMetadata(products) { 182 let productVal = []; 183 for (let i = 0; i < products.length; i++) { 184 productVal.push(products[i].browser_name); 185 } 186 187 const url = new URL('/api/metadata', window.location); 188 url.searchParams.set('includeTestLevel', true); 189 url.searchParams.set('products', productVal.join(',')); 190 this.load( 191 window.fetch(url).then(r => r.json()).then(mergedMetadata => { 192 this.mergedMetadata = mergedMetadata; 193 }) 194 ); 195 } 196 197 // loadPendingMetadata is called when wpt-metadata.js is initialized 198 // through constructor() or when users triage new metadata, unlike loadMergedMetadata(). 199 loadPendingMetadata() { 200 const url = new URL('/api/metadata/pending', window.location); 201 this.load( 202 window.fetch(url).then(r => r.json()).then(pendingMetadata => { 203 this.pendingMetadata = pendingMetadata; 204 }) 205 ); 206 } 207 208 computeMetadata(mergedMetadata, pendingMetadata) { 209 if (!mergedMetadata || !pendingMetadata) { 210 return; 211 } 212 const metadata = Object.assign({}, mergedMetadata); 213 for (const testname of Object.keys(pendingMetadata)) { 214 if (testname in metadata) { 215 metadata[testname] = metadata[testname].concat(pendingMetadata[testname]); 216 } else { 217 metadata[testname] = pendingMetadata[testname]; 218 } 219 } 220 return metadata; 221 } 222 223 computeTestResultSet(searchResults) { 224 if (!searchResults || !searchResults.length) { 225 return; 226 } 227 228 const testResultSet = new Set(); 229 for (const result of searchResults) { 230 let test = result.test; 231 // Add all ancestor directories of test into testResultSet. 232 // getDirname eventually returns an empty string at the root to terminate the loop. 233 while (test !== '') { 234 testResultSet.add(test); 235 test = this.getDirname(test); 236 } 237 } 238 return testResultSet; 239 } 240 241 appendTestLabel(testname, labelMap, label) { 242 if (!label || label === '') { 243 return; 244 } 245 246 if ((testname in labelMap) === false) { 247 labelMap[testname] = label; 248 } else { 249 labelMap[testname] = labelMap[testname] + ',' + label; 250 } 251 } 252 253 computeDisplayedMetadata(path, metadata, testResultSet) { 254 if (!metadata || !path || !testResultSet) { 255 return; 256 } 257 258 // This loop constructs both the metadataMap, which is used to show inline 259 // bug icons in the test results, and displayedMetdata, which is the list of 260 // metadata links shown at the bottom of the page. 261 let metadataMap = {}; 262 let labelMap = {}; 263 let displayedMetadata = []; 264 for (const test of Object.keys(metadata).filter(k => this.shouldShowMetadata(k, path, testResultSet))) { 265 const seenProductURLs = new Set(); 266 for (const link of metadata[test]) { 267 if (link.url === '') { 268 if (link.product === '') { 269 this.appendTestLabel(test, labelMap, link.label); 270 } 271 continue; 272 } 273 const urlHref = this.getUrlHref(link.url); 274 const subtestMap = {}; 275 if ('results' in link) { 276 for (const resultEntry of link['results']) { 277 if ('subtest' in resultEntry) { 278 subtestMap[resultEntry['subtest']] = urlHref; 279 } 280 } 281 } 282 283 const metadataMapKey = test + link.product; 284 if ((metadataMapKey in metadataMap) === false) { 285 metadataMap[metadataMapKey] = {}; 286 } 287 288 if (Object.keys(subtestMap).length === 0) { 289 // When there is no subtest, it is a test-level URL. 290 metadataMap[metadataMapKey]['/'] = urlHref; 291 this.appendTestLabel(test, labelMap, link.label); 292 } else { 293 metadataMap[metadataMapKey] = Object.assign(metadataMap[metadataMapKey], subtestMap); 294 } 295 296 // Avoid showing duplicate bug links in the list of metadata shown at the bottom of the page. 297 const serializedProductURL = link.product.trim() + '_' + link.url.trim(); 298 if (seenProductURLs.has(serializedProductURL)) { 299 continue; 300 } 301 seenProductURLs.add(serializedProductURL); 302 const wptMetadataNode = { 303 test, 304 url: urlHref, 305 product: link.product, 306 }; 307 displayedMetadata.push(wptMetadataNode); 308 } 309 } 310 311 this.labelMap = labelMap; 312 this.metadataMap = metadataMap; 313 this._resetSelectors(); 314 return displayedMetadata; 315 } 316 317 computeFirstThree(displayedMetadata) { 318 return displayedMetadata && displayedMetadata.length && displayedMetadata.slice(0, 3); 319 } 320 321 computeOthers(displayedMetadata) { 322 if (!displayedMetadata || displayedMetadata.length < 4) { 323 return null; 324 } 325 return displayedMetadata.slice(3); 326 } 327 328 getUrlHref(url) { 329 const httpsPrefix = 'https://'; 330 const httpPrefix = 'http://'; 331 if (!(url.startsWith(httpsPrefix) || url.startsWith(httpPrefix))) { 332 return httpsPrefix + url; 333 } 334 return url; 335 } 336 337 handleOpenCollapsible() { 338 this.shadowRoot.querySelector('#metadata-toggle').hidden = true; 339 this.shadowRoot.querySelector('#metadata-collapsible').opened = true; 340 } 341 342 shouldShowMetadata(metadataTestName, path, testResultSet) { 343 let curPath = path; 344 if (this.pathIsASubfolder) { 345 curPath = curPath + '/'; 346 } 347 348 if (metadataTestName.endsWith('/*')) { 349 const metadataDirname = metadataTestName.substring(0, metadataTestName.length - 1); 350 const metadataDirnameWithoutSlash = metadataTestName.substring(0, metadataTestName.length - 2); 351 return ( 352 // whether metadataDirname is an ancestor of curPath 353 curPath.startsWith(metadataDirname) || 354 // whether metadataDirname is in the current directory and included by searchResults 355 (this.isParentDir(curPath, metadataDirname) && testResultSet.has(metadataDirnameWithoutSlash)) 356 ); 357 } 358 return metadataTestName.startsWith(curPath) && testResultSet.has(metadataTestName); 359 } 360 } 361 window.customElements.define(WPTMetadata.is, WPTMetadata); 362 363 export { WPTMetadataNode, WPTMetadata };