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 };