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