|
| 1 | +/** |
| 2 | + * @license Copyright 2019 Google Inc. All Rights Reserved. |
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 4 | + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. |
| 5 | + */ |
| 6 | +'use strict' |
| 7 | + |
| 8 | +const { |
| 9 | + computeRepresentativeRuns, |
| 10 | +} = require('@lhci/utils/src/representative-runs') |
| 11 | + |
| 12 | +/** @typedef {(lhrs: Array<LH.Result>) => ({value: number})} StatisticFn */ |
| 13 | + |
| 14 | +/** |
| 15 | + * @return {StatisticFn} |
| 16 | + */ |
| 17 | +function metaLighthouseVersion() { |
| 18 | + return lhrs => { |
| 19 | + const version = lhrs[0].lighthouseVersion || '' |
| 20 | + const [_, major = '0', minor = '0', patch = '0'] = |
| 21 | + version.match(/^(\d+)\.(\d+)\.(\d+)/) || [] |
| 22 | + const versionAsNumber = |
| 23 | + Number(major) * 100 * 100 + Number(minor) * 100 + Number(patch) |
| 24 | + return { value: versionAsNumber || 0 } |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +/** @param {Array<number>} values */ |
| 29 | +function median(values) { |
| 30 | + const sorted = [...values].sort((a, b) => a - b) |
| 31 | + const medianIndex = Math.floor(values.length / 2) |
| 32 | + |
| 33 | + if (values.length === 0) return { value: -1 } |
| 34 | + return { value: sorted[medianIndex] } |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * @param {string} auditId |
| 39 | + * @return {StatisticFn} |
| 40 | + */ |
| 41 | +function auditNumericValueMedian(auditId) { |
| 42 | + return lhrs => { |
| 43 | + const values = lhrs |
| 44 | + .map(lhr => lhr.audits[auditId] && lhr.audits[auditId].numericValue) |
| 45 | + .filter( |
| 46 | + /** @return {value is number} */ value => |
| 47 | + typeof value === 'number' && Number.isFinite(value), |
| 48 | + ) |
| 49 | + |
| 50 | + return median(values) |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +/** |
| 55 | + * @param {string} categoryId |
| 56 | + * @return {StatisticFn} |
| 57 | + */ |
| 58 | +function categoryScoreMedian(categoryId) { |
| 59 | + return lhrs => { |
| 60 | + const values = lhrs |
| 61 | + .map( |
| 62 | + lhr => lhr.categories[categoryId] && lhr.categories[categoryId].score, |
| 63 | + ) |
| 64 | + .filter( |
| 65 | + /** @return {value is number} */ value => |
| 66 | + typeof value === 'number' && Number.isFinite(value), |
| 67 | + ) |
| 68 | + |
| 69 | + return median(values) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * @param {string} categoryId |
| 75 | + * @param {'min'|'max'} type |
| 76 | + * @return {StatisticFn} |
| 77 | + */ |
| 78 | +function categoryScoreMinOrMax(categoryId, type) { |
| 79 | + return lhrs => { |
| 80 | + const values = lhrs |
| 81 | + .map( |
| 82 | + lhr => lhr.categories[categoryId] && lhr.categories[categoryId].score, |
| 83 | + ) |
| 84 | + .filter( |
| 85 | + /** @return {value is number} */ value => |
| 86 | + typeof value === 'number' && Number.isFinite(value), |
| 87 | + ) |
| 88 | + |
| 89 | + if (!values.length) return { value: -1 } |
| 90 | + return { value: Math[type](...values) } |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +/** |
| 95 | + * @param {string} groupId |
| 96 | + * @param {'pass'|'fail'|'na'} type |
| 97 | + * @return {StatisticFn} |
| 98 | + */ |
| 99 | +function auditGroupCountOfMedianLhr(groupId, type) { |
| 100 | + return lhrs => { |
| 101 | + const [medianLhr] = computeRepresentativeRuns([lhrs.map(lhr => [lhr, lhr])]) |
| 102 | + if (!medianLhr) return { value: -1 } |
| 103 | + |
| 104 | + // Start out with -1 as "no data available" |
| 105 | + let count = -1 |
| 106 | + for (const category of Object.values(medianLhr.categories)) { |
| 107 | + for (const auditRef of category.auditRefs || []) { |
| 108 | + if (auditRef.group !== groupId) continue |
| 109 | + const audit = medianLhr.audits[auditRef.id] |
| 110 | + if (!audit) continue |
| 111 | + |
| 112 | + // Once we find our first candidate audit, set the count to 0. |
| 113 | + if (count === -1) count = 0 |
| 114 | + |
| 115 | + const { score, scoreDisplayMode } = audit |
| 116 | + if (scoreDisplayMode === 'informative' && type === 'na') count++ |
| 117 | + if (scoreDisplayMode === 'notApplicable' && type === 'na') count++ |
| 118 | + if (scoreDisplayMode === 'binary' && score === 1 && type === 'pass') |
| 119 | + count++ |
| 120 | + if (scoreDisplayMode === 'binary' && score !== 1 && type === 'fail') |
| 121 | + count++ |
| 122 | + if (scoreDisplayMode === 'error' && type === 'fail') count++ |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + return { value: count } |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +/** @type {Record<LHCI.ServerCommand.StatisticName, StatisticFn>} */ |
| 131 | +const definitions = { |
| 132 | + meta_lighthouse_version: metaLighthouseVersion(), |
| 133 | + audit_interactive_median: auditNumericValueMedian('interactive'), |
| 134 | + 'audit_speed-index_median': auditNumericValueMedian('speed-index'), |
| 135 | + 'audit_first-contentful-paint_median': auditNumericValueMedian( |
| 136 | + 'first-contentful-paint', |
| 137 | + ), |
| 138 | + 'audit_largest-contentful-paint_median': auditNumericValueMedian( |
| 139 | + 'largest-contentful-paint', |
| 140 | + ), |
| 141 | + 'audit_total-blocking-time_median': auditNumericValueMedian( |
| 142 | + 'total-blocking-time', |
| 143 | + ), |
| 144 | + 'audit_max-potential-fid_median': |
| 145 | + auditNumericValueMedian('max-potential-fid'), |
| 146 | + category_performance_median: categoryScoreMedian('performance'), |
| 147 | + category_pwa_median: categoryScoreMedian('pwa'), |
| 148 | + category_seo_median: categoryScoreMedian('seo'), |
| 149 | + category_accessibility_median: categoryScoreMedian('accessibility'), |
| 150 | + 'category_best-practices_median': categoryScoreMedian('best-practices'), |
| 151 | + category_performance_min: categoryScoreMinOrMax('performance', 'min'), |
| 152 | + category_pwa_min: categoryScoreMinOrMax('pwa', 'min'), |
| 153 | + category_seo_min: categoryScoreMinOrMax('seo', 'min'), |
| 154 | + category_accessibility_min: categoryScoreMinOrMax('accessibility', 'min'), |
| 155 | + 'category_best-practices_min': categoryScoreMinOrMax('best-practices', 'min'), |
| 156 | + category_performance_max: categoryScoreMinOrMax('performance', 'max'), |
| 157 | + category_pwa_max: categoryScoreMinOrMax('pwa', 'max'), |
| 158 | + category_seo_max: categoryScoreMinOrMax('seo', 'max'), |
| 159 | + category_accessibility_max: categoryScoreMinOrMax('accessibility', 'max'), |
| 160 | + 'category_best-practices_max': categoryScoreMinOrMax('best-practices', 'max'), |
| 161 | + 'auditgroup_pwa-fast-reliable_pass': auditGroupCountOfMedianLhr( |
| 162 | + 'pwa-fast-reliable', |
| 163 | + 'pass', |
| 164 | + ), |
| 165 | + 'auditgroup_pwa-fast-reliable_fail': auditGroupCountOfMedianLhr( |
| 166 | + 'pwa-fast-reliable', |
| 167 | + 'fail', |
| 168 | + ), |
| 169 | + 'auditgroup_pwa-fast-reliable_na': auditGroupCountOfMedianLhr( |
| 170 | + 'pwa-fast-reliable', |
| 171 | + 'na', |
| 172 | + ), |
| 173 | + 'auditgroup_pwa-installable_pass': auditGroupCountOfMedianLhr( |
| 174 | + 'pwa-installable', |
| 175 | + 'pass', |
| 176 | + ), |
| 177 | + 'auditgroup_pwa-installable_fail': auditGroupCountOfMedianLhr( |
| 178 | + 'pwa-installable', |
| 179 | + 'fail', |
| 180 | + ), |
| 181 | + 'auditgroup_pwa-installable_na': auditGroupCountOfMedianLhr( |
| 182 | + 'pwa-installable', |
| 183 | + 'na', |
| 184 | + ), |
| 185 | + 'auditgroup_pwa-optimized_pass': auditGroupCountOfMedianLhr( |
| 186 | + 'pwa-optimized', |
| 187 | + 'pass', |
| 188 | + ), |
| 189 | + 'auditgroup_pwa-optimized_fail': auditGroupCountOfMedianLhr( |
| 190 | + 'pwa-optimized', |
| 191 | + 'fail', |
| 192 | + ), |
| 193 | + 'auditgroup_pwa-optimized_na': auditGroupCountOfMedianLhr( |
| 194 | + 'pwa-optimized', |
| 195 | + 'na', |
| 196 | + ), |
| 197 | + 'auditgroup_a11y-best-practices_pass': auditGroupCountOfMedianLhr( |
| 198 | + 'a11y-best-practices', |
| 199 | + 'pass', |
| 200 | + ), |
| 201 | + 'auditgroup_a11y-best-practices_fail': auditGroupCountOfMedianLhr( |
| 202 | + 'a11y-best-practices', |
| 203 | + 'fail', |
| 204 | + ), |
| 205 | + 'auditgroup_a11y-best-practices_na': auditGroupCountOfMedianLhr( |
| 206 | + 'a11y-best-practices', |
| 207 | + 'na', |
| 208 | + ), |
| 209 | + 'auditgroup_a11y-color-contrast_pass': auditGroupCountOfMedianLhr( |
| 210 | + 'a11y-color-contrast', |
| 211 | + 'pass', |
| 212 | + ), |
| 213 | + 'auditgroup_a11y-color-contrast_fail': auditGroupCountOfMedianLhr( |
| 214 | + 'a11y-color-contrast', |
| 215 | + 'fail', |
| 216 | + ), |
| 217 | + 'auditgroup_a11y-color-contrast_na': auditGroupCountOfMedianLhr( |
| 218 | + 'a11y-color-contrast', |
| 219 | + 'na', |
| 220 | + ), |
| 221 | + 'auditgroup_a11y-names-labels_pass': auditGroupCountOfMedianLhr( |
| 222 | + 'a11y-names-labels', |
| 223 | + 'pass', |
| 224 | + ), |
| 225 | + 'auditgroup_a11y-names-labels_fail': auditGroupCountOfMedianLhr( |
| 226 | + 'a11y-names-labels', |
| 227 | + 'fail', |
| 228 | + ), |
| 229 | + 'auditgroup_a11y-names-labels_na': auditGroupCountOfMedianLhr( |
| 230 | + 'a11y-names-labels', |
| 231 | + 'na', |
| 232 | + ), |
| 233 | + 'auditgroup_a11y-navigation_pass': auditGroupCountOfMedianLhr( |
| 234 | + 'a11y-navigation', |
| 235 | + 'pass', |
| 236 | + ), |
| 237 | + 'auditgroup_a11y-navigation_fail': auditGroupCountOfMedianLhr( |
| 238 | + 'a11y-navigation', |
| 239 | + 'fail', |
| 240 | + ), |
| 241 | + 'auditgroup_a11y-navigation_na': auditGroupCountOfMedianLhr( |
| 242 | + 'a11y-navigation', |
| 243 | + 'na', |
| 244 | + ), |
| 245 | + 'auditgroup_a11y-aria_pass': auditGroupCountOfMedianLhr('a11y-aria', 'pass'), |
| 246 | + 'auditgroup_a11y-aria_fail': auditGroupCountOfMedianLhr('a11y-aria', 'fail'), |
| 247 | + 'auditgroup_a11y-aria_na': auditGroupCountOfMedianLhr('a11y-aria', 'na'), |
| 248 | + 'auditgroup_a11y-language_pass': auditGroupCountOfMedianLhr( |
| 249 | + 'a11y-language', |
| 250 | + 'pass', |
| 251 | + ), |
| 252 | + 'auditgroup_a11y-language_fail': auditGroupCountOfMedianLhr( |
| 253 | + 'a11y-language', |
| 254 | + 'fail', |
| 255 | + ), |
| 256 | + 'auditgroup_a11y-language_na': auditGroupCountOfMedianLhr( |
| 257 | + 'a11y-language', |
| 258 | + 'na', |
| 259 | + ), |
| 260 | + 'auditgroup_a11y-audio-video_pass': auditGroupCountOfMedianLhr( |
| 261 | + 'a11y-audio-video', |
| 262 | + 'pass', |
| 263 | + ), |
| 264 | + 'auditgroup_a11y-audio-video_fail': auditGroupCountOfMedianLhr( |
| 265 | + 'a11y-audio-video', |
| 266 | + 'fail', |
| 267 | + ), |
| 268 | + 'auditgroup_a11y-audio-video_na': auditGroupCountOfMedianLhr( |
| 269 | + 'a11y-audio-video', |
| 270 | + 'na', |
| 271 | + ), |
| 272 | + 'auditgroup_a11y-tables-lists_pass': auditGroupCountOfMedianLhr( |
| 273 | + 'a11y-tables-lists', |
| 274 | + 'pass', |
| 275 | + ), |
| 276 | + 'auditgroup_a11y-tables-lists_fail': auditGroupCountOfMedianLhr( |
| 277 | + 'a11y-tables-lists', |
| 278 | + 'fail', |
| 279 | + ), |
| 280 | + 'auditgroup_a11y-tables-lists_na': auditGroupCountOfMedianLhr( |
| 281 | + 'a11y-tables-lists', |
| 282 | + 'na', |
| 283 | + ), |
| 284 | + 'auditgroup_seo-mobile_pass': auditGroupCountOfMedianLhr( |
| 285 | + 'seo-mobile', |
| 286 | + 'pass', |
| 287 | + ), |
| 288 | + 'auditgroup_seo-mobile_fail': auditGroupCountOfMedianLhr( |
| 289 | + 'seo-mobile', |
| 290 | + 'fail', |
| 291 | + ), |
| 292 | + 'auditgroup_seo-mobile_na': auditGroupCountOfMedianLhr('seo-mobile', 'na'), |
| 293 | + 'auditgroup_seo-content_pass': auditGroupCountOfMedianLhr( |
| 294 | + 'seo-content', |
| 295 | + 'pass', |
| 296 | + ), |
| 297 | + 'auditgroup_seo-content_fail': auditGroupCountOfMedianLhr( |
| 298 | + 'seo-content', |
| 299 | + 'fail', |
| 300 | + ), |
| 301 | + 'auditgroup_seo-content_na': auditGroupCountOfMedianLhr('seo-content', 'na'), |
| 302 | + 'auditgroup_seo-crawl_pass': auditGroupCountOfMedianLhr('seo-crawl', 'pass'), |
| 303 | + 'auditgroup_seo-crawl_fail': auditGroupCountOfMedianLhr('seo-crawl', 'fail'), |
| 304 | + 'auditgroup_seo-crawl_na': auditGroupCountOfMedianLhr('seo-crawl', 'na'), |
| 305 | +} |
| 306 | + |
| 307 | +// Keep the export separate from declaration to enable tsc to typecheck the `@type` annotation. |
| 308 | +module.exports = { definitions, VERSION: 2 } |
0 commit comments