lighthouse 11.4.0-dev.20240101 → 11.4.0-dev.20240103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,11 +38,11 @@ export class CategoryRenderer {
38
38
 
39
39
  /**
40
40
  * @param {LH.ReportResult.AuditRef} audit
41
- * @return {Element}
41
+ * @return {HTMLElement}
42
42
  */
43
43
  renderAudit(audit) {
44
44
  const component = this.dom.createComponent('audit');
45
- return this.populateAuditValues(audit, component);
45
+ return /** @type {HTMLElement} */ (this.populateAuditValues(audit, component));
46
46
  }
47
47
 
48
48
  /**
@@ -298,10 +298,10 @@ export class CategoryRenderer {
298
298
  * Take a set of audits and render in a top-level, expandable clump that starts
299
299
  * in a collapsed state.
300
300
  * @param {Exclude<TopLevelClumpId, 'failed'>} clumpId
301
- * @param {{auditRefs: Array<LH.ReportResult.AuditRef>, description?: string, openByDefault?: boolean}} clumpOpts
301
+ * @param {{auditRefsOrEls: Array<LH.ReportResult.AuditRef | HTMLElement>, description?: string, openByDefault?: boolean}} clumpOpts
302
302
  * @return {!Element}
303
303
  */
304
- renderClump(clumpId, {auditRefs, description, openByDefault}) {
304
+ renderClump(clumpId, {auditRefsOrEls, description, openByDefault}) {
305
305
  const clumpComponent = this.dom.createComponent('clump');
306
306
  const clumpElement = this.dom.find('.lh-clump', clumpComponent);
307
307
 
@@ -314,10 +314,16 @@ export class CategoryRenderer {
314
314
  this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
315
315
 
316
316
  const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
317
- itemCountEl.textContent = `(${auditRefs.length})`;
317
+ itemCountEl.textContent = `(${auditRefsOrEls.length})`;
318
318
 
319
319
  // Add all audit results to the clump.
320
- const auditElements = auditRefs.map(this.renderAudit.bind(this));
320
+ const auditElements = auditRefsOrEls.map(audit => {
321
+ if (audit instanceof HTMLElement) {
322
+ return audit;
323
+ } else {
324
+ return this.renderAudit(audit);
325
+ }
326
+ });
321
327
  clumpElement.append(...auditElements);
322
328
 
323
329
  const el = this.dom.find('.lh-audit-group', clumpComponent);
@@ -564,7 +570,11 @@ export class CategoryRenderer {
564
570
  // Expand on warning, or manual audits when there are no failing audits.
565
571
  const openByDefault =
566
572
  clumpId === 'warning' || (clumpId === 'manual' && numFailingAudits === 0);
567
- const clumpElem = this.renderClump(clumpId, {auditRefs, description, openByDefault});
573
+ const clumpElem = this.renderClump(clumpId, {
574
+ auditRefsOrEls: auditRefs,
575
+ description,
576
+ openByDefault,
577
+ });
568
578
  element.append(clumpElem);
569
579
  }
570
580
 
@@ -46,9 +46,11 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
46
46
  * Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
47
47
  * @param {LH.ReportResult.AuditRef[]} filterableMetrics
48
48
  * @param {HTMLDivElement} categoryEl
49
+ * @param {(acronym: FilterType) => void} onFilterChange
49
50
  */
50
- renderMetricAuditFilter(filterableMetrics: LH.ReportResult.AuditRef[], categoryEl: HTMLDivElement): void;
51
+ renderMetricAuditFilter(filterableMetrics: LH.ReportResult.AuditRef[], categoryEl: HTMLDivElement, onFilterChange: (acronym: FilterType) => void): void;
51
52
  }
52
53
  export type DOM = import('./dom.js').DOM;
54
+ export type FilterType = LH.Result.MetricAcronym | 'All';
53
55
  import { CategoryRenderer } from './category-renderer.js';
54
56
  //# sourceMappingURL=performance-category-renderer.d.ts.map
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  /** @typedef {import('./dom.js').DOM} DOM */
8
+ /** @typedef {LH.Result.MetricAcronym | 'All'} FilterType */
8
9
 
9
10
  import {CategoryRenderer} from './category-renderer.js';
10
11
  import {ReportUtils} from './report-utils.js';
@@ -211,73 +212,106 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
211
212
  filmstripEl && timelineEl.append(filmstripEl);
212
213
  }
213
214
 
214
- const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
215
- // TODO: only add if there are opportunities & diagnostics rendered.
216
- if (filterableMetrics.length) {
217
- this.renderMetricAuditFilter(filterableMetrics, element);
218
- }
215
+ const allInsights = category.auditRefs
216
+ .filter(audit => this._isPerformanceInsight(audit))
217
+ .map(auditRef => {
218
+ const {overallImpact, overallLinearImpact} = this.overallImpact(auditRef, metricAudits);
219
+ const guidanceLevel = auditRef.result.guidanceLevel || 1;
220
+ const auditEl = this.renderAudit(auditRef);
221
+
222
+ return {auditRef, auditEl, overallImpact, overallLinearImpact, guidanceLevel};
223
+ });
219
224
 
220
225
  // Diagnostics
221
- const diagnosticAudits = category.auditRefs
222
- .filter(audit => this._isPerformanceInsight(audit))
223
- .filter(audit => !ReportUtils.showAsPassed(audit.result));
224
-
225
- /** @type {Array<{auditRef:LH.ReportResult.AuditRef, overallImpact: number, overallLinearImpact: number, guidanceLevel: number}>} */
226
- const auditImpacts = [];
227
- diagnosticAudits.forEach(audit => {
228
- const {
229
- overallImpact: overallImpact,
230
- overallLinearImpact: overallLinearImpact,
231
- } = this.overallImpact(audit, metricAudits);
232
- const guidanceLevel = audit.result.guidanceLevel ?? 1;
233
- auditImpacts.push({auditRef: audit, overallImpact, overallLinearImpact, guidanceLevel});
234
- });
226
+ const diagnosticAudits = allInsights
227
+ .filter(audit => !ReportUtils.showAsPassed(audit.auditRef.result));
228
+
229
+ const passedAudits = allInsights
230
+ .filter(audit => ReportUtils.showAsPassed(audit.auditRef.result));
231
+
232
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
233
+ groupEl.classList.add('lh-audit-group--diagnostics');
234
+
235
+ /**
236
+ * @param {FilterType} acronym
237
+ */
238
+ function refreshFilteredAudits(acronym) {
239
+ for (const audit of allInsights) {
240
+ if (acronym === 'All') {
241
+ audit.auditEl.hidden = false;
242
+ } else {
243
+ const shouldHide = audit.auditRef.result.metricSavings?.[acronym] === undefined;
244
+ audit.auditEl.hidden = shouldHide;
245
+ }
246
+ }
247
+
248
+ diagnosticAudits.sort((a, b) => {
249
+ // If the score display mode is "metricSavings", the `score` will be a coarse indicator of the overall impact.
250
+ // Therefore, it makes sense to sort audits by score first to ensure visual clarity with the score icons.
251
+ const scoreA = a.auditRef.result.scoreDisplayMode === 'informative'
252
+ ? 100
253
+ : Number(a.auditRef.result.score);
254
+ const scoreB = b.auditRef.result.scoreDisplayMode === 'informative'
255
+ ? 100
256
+ : Number(b.auditRef.result.score);
257
+ if (scoreA !== scoreB) return scoreA - scoreB;
258
+
259
+ // If there is a metric filter applied, we should sort by the impact to that specific metric.
260
+ if (acronym !== 'All') {
261
+ const aSavings = a.auditRef.result.metricSavings?.[acronym] ?? -1;
262
+ const bSavings = b.auditRef.result.metricSavings?.[acronym] ?? -1;
263
+ if (aSavings !== bSavings) return bSavings - aSavings;
264
+ }
265
+
266
+ // Overall impact is the estimated improvement to the performance score
267
+ if (a.overallImpact !== b.overallImpact) {
268
+ return b.overallImpact * b.guidanceLevel - a.overallImpact * a.guidanceLevel;
269
+ }
270
+
271
+ // Fall back to the linear impact if the normal impact is rounded down to 0
272
+ if (
273
+ a.overallImpact === 0 && b.overallImpact === 0 &&
274
+ a.overallLinearImpact !== b.overallLinearImpact
275
+ ) {
276
+ return b.overallLinearImpact * b.guidanceLevel - a.overallLinearImpact * a.guidanceLevel;
277
+ }
278
+
279
+ // Audits that have no estimated savings should be prioritized by the guidance level
280
+ return b.guidanceLevel - a.guidanceLevel;
281
+ });
235
282
 
236
- auditImpacts.sort((a, b) => {
237
- // Performance diagnostics should only have score display modes of "informative" and "metricSavings"
238
- // If the score display mode is "metricSavings", the `score` will be a coarse approximation of the overall impact.
239
- // Therefore, it makes sense to sort audits by score first to ensure visual clarity with the score icons.
240
- const scoreA = a.auditRef.result.scoreDisplayMode === 'informative'
241
- ? 100
242
- : Number(a.auditRef.result.score);
243
- const scoreB = b.auditRef.result.scoreDisplayMode === 'informative'
244
- ? 100
245
- : Number(b.auditRef.result.score);
246
- if (scoreA !== scoreB) return scoreA - scoreB;
247
-
248
- // Overall impact is the estimated improvement to the performance score
249
- if (a.overallImpact !== b.overallImpact) {
250
- return b.overallImpact * b.guidanceLevel - a.overallImpact * a.guidanceLevel;
283
+ for (const audit of diagnosticAudits) {
284
+ groupEl.insertBefore(audit.auditEl, footerEl);
251
285
  }
286
+ }
252
287
 
253
- // Fall back to the linear impact if the normal impact is rounded down to 0
254
- if (
255
- a.overallImpact === 0 && b.overallImpact === 0 &&
256
- a.overallLinearImpact !== b.overallLinearImpact
257
- ) {
258
- return b.overallLinearImpact * b.guidanceLevel - a.overallLinearImpact * a.guidanceLevel;
288
+ /** @type {Set<string>} */
289
+ const filterableMetricAcronyms = new Set();
290
+ for (const audit of diagnosticAudits) {
291
+ const metricSavings = audit.auditRef.result.metricSavings || {};
292
+ for (const [key, value] of Object.entries(metricSavings)) {
293
+ if (typeof value === 'number') filterableMetricAcronyms.add(key);
259
294
  }
295
+ }
260
296
 
261
- // Audits that have no estimated savings should be prioritized by the guidance level
262
- return b.guidanceLevel - a.guidanceLevel;
263
- });
297
+ const filterableMetrics =
298
+ metricAudits.filter(a => a.acronym && filterableMetricAcronyms.has(a.acronym));
264
299
 
265
- if (auditImpacts.length) {
266
- const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
267
- auditImpacts.forEach(item => groupEl.insertBefore(this.renderAudit(item.auditRef), footerEl));
268
- groupEl.classList.add('lh-audit-group--diagnostics');
269
- element.append(groupEl);
300
+ // TODO: only add if there are opportunities & diagnostics rendered.
301
+ if (filterableMetrics.length) {
302
+ this.renderMetricAuditFilter(filterableMetrics, element, refreshFilteredAudits);
270
303
  }
271
304
 
272
- // Passed audits
273
- const passedAudits = category.auditRefs
274
- .filter(audit =>
275
- this._isPerformanceInsight(audit) && ReportUtils.showAsPassed(audit.result));
305
+ refreshFilteredAudits('All');
306
+
307
+ if (diagnosticAudits.length) {
308
+ element.append(groupEl);
309
+ }
276
310
 
277
311
  if (!passedAudits.length) return element;
278
312
 
279
313
  const clumpOpts = {
280
- auditRefs: passedAudits,
314
+ auditRefsOrEls: passedAudits.map(audit => audit.auditEl),
281
315
  groupDefinitions: groups,
282
316
  };
283
317
  const passedElem = this.renderClump('passed', clumpOpts);
@@ -318,16 +352,17 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
318
352
  * Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
319
353
  * @param {LH.ReportResult.AuditRef[]} filterableMetrics
320
354
  * @param {HTMLDivElement} categoryEl
355
+ * @param {(acronym: FilterType) => void} onFilterChange
321
356
  */
322
- renderMetricAuditFilter(filterableMetrics, categoryEl) {
357
+ renderMetricAuditFilter(filterableMetrics, categoryEl, onFilterChange) {
323
358
  const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
324
359
  const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
325
360
  textEl.textContent = Globals.strings.showRelevantAudits;
326
361
 
327
- const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
328
- ({acronym: 'All'}),
362
+ const filterChoices = [
363
+ /** @type {const} */ ({acronym: 'All'}),
329
364
  ...filterableMetrics,
330
- ]);
365
+ ];
331
366
 
332
367
  // Form labels need to reference unique IDs, but multiple reports rendered in the same DOM (eg PSI)
333
368
  // would mean ID conflict. To address this, we 'scope' these radio inputs with a unique suffix.
@@ -341,7 +376,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
341
376
 
342
377
  const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label');
343
378
  labelEl.htmlFor = elemId;
344
- labelEl.title = metric.result?.title;
379
+ labelEl.title = 'result' in metric ? metric.result.title : '';
345
380
  labelEl.textContent = metric.acronym || metric.id;
346
381
 
347
382
  if (metric.acronym === 'All') {
@@ -357,17 +392,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
357
392
  }
358
393
  categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
359
394
 
360
- for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
361
- if (metric.acronym === 'All') {
362
- perfAuditEl.hidden = false;
363
- continue;
364
- }
365
-
366
- perfAuditEl.hidden = true;
367
- if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
368
- perfAuditEl.hidden = false;
369
- }
370
- }
395
+ onFilterChange(metric.acronym || 'All');
371
396
 
372
397
  // Hide groups/clumps if all child audits are also hidden.
373
398
  const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
@@ -29,8 +29,11 @@ export class PwaCategoryRenderer extends CategoryRenderer {
29
29
 
30
30
  // Manual audits are still in a manual clump.
31
31
  const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
32
- const manualElem = this.renderClump('manual',
33
- {auditRefs: manualAuditRefs, description: category.manualDescription, openByDefault: true});
32
+ const manualElem = this.renderClump('manual', {
33
+ auditRefsOrEls: manualAuditRefs,
34
+ description: category.manualDescription,
35
+ openByDefault: true,
36
+ });
34
37
  categoryElem.append(manualElem);
35
38
 
36
39
  return categoryElem;
package/types/config.d.ts CHANGED
@@ -9,6 +9,7 @@ import {Audit} from '../core/audits/audit.js';
9
9
  import {SharedFlagsSettings, ConfigSettings} from './lhr/settings.js';
10
10
  import Gatherer from './gatherer.js';
11
11
  import {IcuMessage} from './lhr/i18n.js';
12
+ import Result from './lhr/lhr.js';
12
13
 
13
14
  interface ClassOf<T> {
14
15
  new (): T;
@@ -81,7 +82,7 @@ declare module Config {
81
82
  id: string;
82
83
  weight: number;
83
84
  group?: string;
84
- acronym?: string;
85
+ acronym?: Result.MetricAcronym;
85
86
  relevantAudits?: string[];
86
87
  }
87
88
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {FormattedIcu} from './i18n.js';
8
8
  import AuditDetails from './audit-details.js';
9
+ import LHResult from './lhr.js';
9
10
 
10
11
  interface ScoreDisplayModes {
11
12
  /** Scores of 0-1 (map to displayed scores of 0-100). */
@@ -31,13 +32,9 @@ interface ScoreDisplayModes {
31
32
 
32
33
  type ScoreDisplayMode = ScoreDisplayModes[keyof ScoreDisplayModes];
33
34
 
34
- interface MetricSavings {
35
- LCP?: number;
36
- FCP?: number;
37
- CLS?: number;
38
- TBT?: number;
39
- INP?: number;
40
- }
35
+ export type MetricSavings = {
36
+ [M in LHResult.MetricAcronym]?: number;
37
+ };
41
38
 
42
39
  /** Audit result returned in Lighthouse report. All audits offer a description and score of 0-1. */
43
40
  export interface Result {
@@ -115,7 +115,7 @@ declare module Result {
115
115
  /** Optional grouping within the category. Matches the key of a Result.Group. */
116
116
  group?: string;
117
117
  /** The conventional acronym for the audit/metric. */
118
- acronym?: string;
118
+ acronym?: MetricAcronym;
119
119
  /** Any audit IDs closely relevant to this one. */
120
120
  relevantAudits?: string[];
121
121
  }
@@ -197,6 +197,8 @@ declare module Result {
197
197
 
198
198
  /** Gather mode used to collect artifacts. */
199
199
  type GatherMode = 'navigation'|'timespan'|'snapshot';
200
+
201
+ type MetricAcronym = 'FCP' | 'LCP' | 'TBT' | 'CLS' | 'INP' | 'SI' | 'TTI' | 'FMP';
200
202
  }
201
203
 
202
204
  export default Result;