github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/test-results-history-timeline.js (about)

     1  /**
     2   * Copyright 2023 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 { PolymerElement, html } from '../node_modules/@polymer/polymer/polymer-element.js';
     8  const pageStyle = getComputedStyle(document.body);
     9  import { PathInfo } from './path.js';
    10  
    11  const PASS_COLOR = pageStyle.getPropertyValue('--paper-green-300');
    12  const FAIL_COLOR = pageStyle.getPropertyValue('--paper-red-300');
    13  const NEUTRAL_COLOR = pageStyle.getPropertyValue('--paper-grey-300');
    14  const COLOR_MAPPING = {
    15    // Passing statuses
    16    OK: PASS_COLOR,
    17    PASS: PASS_COLOR,
    18  
    19    // Failing statuses
    20    CRASHED: FAIL_COLOR,
    21    ERROR: FAIL_COLOR,
    22    FAIL: FAIL_COLOR,
    23    NOTRUN: FAIL_COLOR,
    24    PRECONDITION_FAILED: FAIL_COLOR,
    25    TIMEOUT: FAIL_COLOR,
    26  
    27    // Neutral statuses
    28    MISSING: NEUTRAL_COLOR,
    29    SKIPPED: NEUTRAL_COLOR,
    30    default: NEUTRAL_COLOR,
    31  };
    32  
    33  const BROWSER_NAMES = [
    34    'chrome',
    35    'edge',
    36    'firefox',
    37    'safari'
    38  ];
    39  
    40  class TestResultsTimeline extends PathInfo(PolymerElement) {
    41    static get template() {
    42      return html`
    43          <style>
    44            .chart rect, .chart text {
    45              cursor: pointer;
    46            }
    47            .browser {
    48              height: 2rem;
    49              margin-bottom: -0.5rem;
    50            }
    51          </style>
    52          <h2>
    53            <img class="browser" alt="chrome chrome,canary,experimental,master,taskcluster,user:chromium-wpt-export-bot,prod logo" src="/static/chrome-canary_64x64.png">
    54            Chrome
    55          </h2>
    56          <div class="chart" id="chromeHistoryChart"></div>
    57  
    58          <h2>
    59            <img class="browser" alt="edge azure,dev,edge,edgechromium,experimental,master,prod logo" src="/static/edge-dev_64x64.png">
    60            Edge
    61          </h2>
    62          <div class="chart" id="edgeHistoryChart"></div>
    63  
    64          <h2>
    65            <img class="browser" alt="firefox experimental,firefox,master,nightly,taskcluster,user:chromium-wpt-export-bot,prod logo" src="/static/firefox-nightly_64x64.png">
    66            Firefox
    67          </h2>
    68          <div class="chart" id="firefoxHistoryChart"></div>
    69  
    70          <h2>
    71            <img class="browser" alt="safari azure,experimental,master,preview,safari,prod logo" src="/static/safari-preview_64x64.png">
    72            Safari
    73          </h2>
    74          <div class="chart" id="safariHistoryChart"></div>
    75          `;
    76    }
    77  
    78    static get properties() {
    79      return {
    80        dataTable: Object,
    81        runIDs: Array,
    82        path: String,
    83        showTestHistory: {
    84          type: Boolean,
    85          value: false,
    86        },
    87        subtestNames: Array,
    88      };
    89    }
    90  
    91    static get observers() {
    92      return [
    93        'displayCharts(showTestHistory, path, subtestNames)',
    94      ];
    95    }
    96  
    97    static get is() {
    98      return 'test-results-history-timeline';
    99    }
   100  
   101    displayCharts(showTestHistory, path, subtestNames) {
   102      if (!path || !showTestHistory || !this.computePathIsATestFile(path)) {
   103        return;
   104      }
   105  
   106      // Get the test history data and then populate the chart
   107      Promise.all([
   108        this.getTestHistory(path),
   109        this.loadCharts()
   110      ]).then(() => this.updateAllCharts(this.historicalData, subtestNames));
   111  
   112      // Google Charts is not responsive, even if one sets a percentage-width, so
   113      // we add a resize observer to redraw the chart if the size changes.
   114      window.addEventListener('resize', () => {
   115        this.updateAllCharts(this.historicalData, subtestNames);
   116      });
   117    }
   118  
   119    // Load Google charts for test history display
   120    async loadCharts() {
   121      await window.google.charts.load('current', { packages: ['timeline'] });
   122    }
   123  
   124    updateAllCharts(historicalData, subtestNames) {
   125      const divNames = [
   126        'chromeHistoryChart',
   127        'edgeHistoryChart',
   128        'firefoxHistoryChart',
   129        'safariHistoryChart'
   130      ];
   131  
   132      // Render charts using an array
   133      this.charts = [null, null, null, null];
   134      // Store run IDs for creating URLs
   135      this.chartRunIDs = [[],[],[],[]];
   136  
   137      divNames.forEach((name, i) => {
   138        this.updateChart(historicalData[BROWSER_NAMES[i]], name, i, subtestNames);
   139      });
   140    }
   141  
   142    updateChart(browserTestData, divID, chartIndex, subtestNames) {
   143      // Our observer may be called before the historical data has been fetched,
   144      // so debounce that.
   145      if (!browserTestData || !subtestNames) {
   146        return;
   147      }
   148  
   149      // Fetching the data table first ensures that Google Charts has been loaded.
   150      // Using timeline chart
   151      // https://developers.google.com/chart/interactive/docs/gallery/timeline
   152      const div = this.$[divID];
   153      this.charts[chartIndex] = new window.google.visualization.Timeline(div);
   154  
   155      this.dataTable = new window.google.visualization.DataTable();
   156  
   157      // Set up columns, including tooltip information and style guidelines
   158      this.dataTable.addColumn({ type: 'string', id: 'Subtest' });
   159      this.dataTable.addColumn({ type: 'string', id: 'Status' });
   160  
   161      // style and tooltip columns that are not displayed
   162      this.dataTable.addColumn({ type: 'string', id: 'style', role: 'style' });
   163      this.dataTable.addColumn({ type: 'string', role: 'tooltip' });
   164  
   165      this.dataTable.addColumn({ type: 'date', id: 'Start' });
   166      this.dataTable.addColumn({ type: 'date', id: 'End' });
   167  
   168      const dataTableRows = [];
   169      const now = new Date();
   170      this.chartRunIDs[chartIndex] = [];
   171  
   172      // Create a row for each subtest
   173      subtestNames.forEach(subtestName => {
   174        if (!browserTestData[subtestName]) {
   175          return;
   176        }
   177        for (let i = 0; i < browserTestData[subtestName].length; i++) {
   178          const dataPoint = browserTestData[subtestName][i];
   179          const startDate = new Date(dataPoint.date);
   180  
   181          // Use the next entry as the end date, or use present time if this
   182          // is the last entry
   183          let endDate = now;
   184          if (i + 1 !== browserTestData[subtestName].length) {
   185            const nextDataPoint = browserTestData[subtestName][i + 1];
   186            endDate = new Date(nextDataPoint.date);
   187          }
   188  
   189          // If this is the main test status, name it based on the amount of subtests
   190          let subtestDisplayName = subtestName;
   191          if (subtestName === '') {
   192            subtestDisplayName = (subtestNames.length > 1) ? 'Harness status' : 'Test status';
   193          }
   194  
   195          const tooltip =
   196            `${dataPoint.status} ${startDate.toLocaleDateString()}-${endDate.toLocaleDateString()}`;
   197          const statusColor = COLOR_MAPPING[dataPoint.status] || COLOR_MAPPING.default;
   198  
   199          // Add the run ID to array of run IDs to use for links
   200          this.chartRunIDs[chartIndex].push(dataPoint.run_id);
   201  
   202          dataTableRows.push([
   203            subtestDisplayName,
   204            dataPoint.status,
   205            statusColor,
   206            tooltip,
   207            startDate,
   208            endDate,
   209          ]);
   210        }
   211      });
   212  
   213      const getChartHeight = numOfSubTests => {
   214        const testHeight = 41;
   215        const xAxisHeight = 50;
   216        if(numOfSubTests <= 30) {
   217          return (numOfSubTests * testHeight) + xAxisHeight;
   218        }
   219        return (20 * testHeight) + xAxisHeight;
   220      };
   221  
   222      let options = {
   223        // height = # of tests * row height + x axis labels height
   224        height: (getChartHeight(this.subtestNames.length)),
   225        tooltip: {
   226          isHtml: false,
   227        },
   228      };
   229      this.dataTable.addRows(dataTableRows);
   230  
   231      // handler to allow rows to be clicked and navigate to the run url
   232      // https://stackoverflow.com/questions/40928971/how-to-customize-google-chart-with-hyperlink-in-the-label
   233      const statusSelectHandler = (chartIndex) => {
   234        const selection = this.charts[chartIndex].getSelection();
   235        if (selection.length > 0) {
   236          const index = selection[0].row;
   237          const runIDs = this.chartRunIDs[chartIndex];
   238  
   239          if (index !== undefined && runIDs.length > index) {
   240            window.open(`/results/?run_id=${runIDs[index]}`, '_blank');
   241          }
   242        }
   243      };
   244      window.google.visualization.events.addListener(
   245        this.charts[chartIndex], 'select', () => statusSelectHandler(chartIndex));
   246  
   247      if (dataTableRows.length > 0) {
   248        this.charts[chartIndex].draw(this.dataTable, options);
   249      } else {
   250        div.innerHTML = 'No browser historical data found for this test.';
   251      }
   252    }
   253  
   254    // get test history and aligned run data
   255    async getTestHistory(path) {
   256      // If there is existing data, clear it to make sure nothing is cached
   257      if(this.historicalData) {
   258        this.historicalData = {};
   259      }
   260  
   261      const options = {
   262        method: 'POST',
   263        headers: {
   264          'Content-Type': 'application/json'
   265        },
   266        body: JSON.stringify({ test_name: path})
   267      };
   268  
   269      this.historicalData = await fetch('/api/history', options)
   270        .then(r => r.json()).then(data => data.results);
   271    }
   272  }
   273  
   274  
   275  window.customElements.define(TestResultsTimeline.is, TestResultsTimeline);
   276  
   277  export { TestResultsTimeline };