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