Skip to content

Commit cbd044e

Browse files
authored
feat(plugin-lighthouse): implement basic audit parsing (#523)
1 parent 116ffb6 commit cbd044e

File tree

2 files changed

+267
-7
lines changed

2 files changed

+267
-7
lines changed

packages/plugin-lighthouse/src/lib/utils.ts

+39-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import type { CliFlags as LighthouseFlags } from 'lighthouse';
2-
import { Audit, Group } from '@code-pushup/models';
1+
import { type CliFlags } from 'lighthouse';
2+
import { Result } from 'lighthouse/types/lhr/audit-result';
3+
import { Audit, AuditOutput, AuditOutputs, Group } from '@code-pushup/models';
34
import { filterItemRefsBy, objectToCliArgs, toArray } from '@code-pushup/utils';
45
import { LIGHTHOUSE_REPORT_NAME } from './constants';
56

67
type RefinedLighthouseOption = {
7-
url: LighthouseFlags['_'];
8-
chromeFlags?: Record<LighthouseFlags['chromeFlags'][number], string>;
8+
url: CliFlags['_'];
9+
chromeFlags?: Record<CliFlags['chromeFlags'][number], string>;
910
};
1011
export type LighthouseCliOptions = RefinedLighthouseOption &
11-
Partial<Omit<LighthouseFlags, keyof RefinedLighthouseOption>>;
12+
Partial<Omit<CliFlags, keyof RefinedLighthouseOption>>;
1213

1314
export function getLighthouseCliArguments(
1415
options: LighthouseCliOptions,
@@ -69,6 +70,38 @@ export function validateOnlyAudits(
6970
return true;
7071
}
7172

73+
export function toAuditOutputs(lhrAudits: Result[]): AuditOutputs {
74+
return lhrAudits.map(
75+
({
76+
id: slug,
77+
score,
78+
numericValue: value = 0, // not every audit has a numericValue
79+
details,
80+
displayValue,
81+
}: Result) => {
82+
const auditOutput: AuditOutput = {
83+
slug,
84+
score: score ?? 1, // score can be null
85+
value,
86+
displayValue,
87+
};
88+
89+
if (details == null) {
90+
return auditOutput;
91+
}
92+
93+
// @TODO implement switch case for detail parsing. Related to #90
94+
const unsupportedType = details.type;
95+
// @TODO use cliui.logger.info Resolve TODO after PR #487 is merged.
96+
console.info(
97+
`Parsing details from type ${unsupportedType} is not implemented.`,
98+
);
99+
100+
return auditOutput;
101+
},
102+
);
103+
}
104+
72105
export class CategoriesNotImplementedError extends Error {
73106
constructor(categorySlugs: string[]) {
74107
super(`categories: "${categorySlugs.join(', ')}" not implemented`);
@@ -91,7 +124,7 @@ export function validateOnlyCategories(
91124
export function filterAuditsAndGroupsByOnlyOptions(
92125
audits: Audit[],
93126
groups: Group[],
94-
options?: Pick<LighthouseFlags, 'onlyAudits' | 'onlyCategories'>,
127+
options?: Pick<CliFlags, 'onlyAudits' | 'onlyCategories'>,
95128
): {
96129
audits: Audit[];
97130
groups: Group[];

packages/plugin-lighthouse/src/lib/utils.unit.test.ts

+228-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { expect } from 'vitest';
1+
import Details from 'lighthouse/types/lhr/audit-details';
2+
import { describe, expect, it } from 'vitest';
23
import {
34
Audit,
45
Group,
@@ -10,6 +11,7 @@ import {
1011
CategoriesNotImplementedError,
1112
filterAuditsAndGroupsByOnlyOptions,
1213
getLighthouseCliArguments,
14+
toAuditOutputs,
1315
validateOnlyAudits,
1416
validateOnlyCategories,
1517
} from './utils';
@@ -359,3 +361,228 @@ describe('filterAuditsAndGroupsByOnlyOptions to be used in plugin config', () =>
359361
).toThrow(new CategoriesNotImplementedError(['missing-category']));
360362
});
361363
});
364+
365+
describe('toAuditOutputs', () => {
366+
it('should parse valid lhr details', () => {
367+
expect(
368+
toAuditOutputs([
369+
{
370+
id: 'first-contentful-paint',
371+
title: 'First Contentful Paint',
372+
description:
373+
'First Contentful Paint marks the time at which the first text or image is painted. [Learn more about the First Contentful Paint metric](https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint/).',
374+
score: 0.55,
375+
scoreDisplayMode: 'numeric',
376+
numericValue: 2838.974,
377+
numericUnit: 'millisecond',
378+
displayValue: '2.8 s',
379+
},
380+
]),
381+
).toStrictEqual([
382+
{
383+
displayValue: '2.8 s',
384+
score: 0.55,
385+
slug: 'first-contentful-paint',
386+
value: 2838.974,
387+
},
388+
]);
389+
});
390+
391+
it('should convert null score to 1', () => {
392+
expect(
393+
toAuditOutputs([
394+
{
395+
id: 'performance-budget',
396+
title: 'Performance budget',
397+
description:
398+
'Keep the quantity and size of network requests under the targets set by the provided performance budget. [Learn more about performance budgets](https://developers.google.com/web/tools/lighthouse/audits/budgets).',
399+
score: null,
400+
scoreDisplayMode: 'notApplicable',
401+
},
402+
]),
403+
).toStrictEqual(
404+
expect.arrayContaining([expect.objectContaining({ score: 1 })]),
405+
);
406+
});
407+
408+
it('should inform that opportunity type is not supported yet', () => {
409+
const outputs = toAuditOutputs([
410+
{
411+
id: 'dummy-audit',
412+
title: 'Dummy Audit',
413+
description: 'This is a dummy audit.',
414+
score: null,
415+
scoreDisplayMode: 'informative',
416+
details: {
417+
type: 'opportunity',
418+
headings: [
419+
{
420+
key: 'url',
421+
valueType: 'url',
422+
label: 'URL',
423+
},
424+
{
425+
key: 'responseTime',
426+
valueType: 'timespanMs',
427+
label: 'Time Spent',
428+
},
429+
],
430+
items: [
431+
{
432+
url: 'https://staging.code-pushup.dev/login',
433+
responseTime: 449.292_000_000_000_03,
434+
},
435+
],
436+
overallSavingsMs: 349.292_000_000_000_03,
437+
} satisfies Details.Opportunity,
438+
},
439+
]);
440+
441+
expect(outputs[0]?.details).toBeUndefined();
442+
});
443+
444+
it('should inform that table type is not supported yet', () => {
445+
const outputs = toAuditOutputs([
446+
{
447+
id: 'dummy-audit',
448+
title: 'Dummy Audit',
449+
description: 'This is a dummy audit.',
450+
score: null,
451+
scoreDisplayMode: 'informative',
452+
details: {
453+
type: 'table',
454+
headings: [],
455+
items: [],
456+
},
457+
},
458+
]);
459+
460+
expect(outputs[0]?.details).toBeUndefined();
461+
});
462+
463+
it('should inform that debugdata type is not supported yet', () => {
464+
const outputs = toAuditOutputs([
465+
{
466+
id: 'cumulative-layout-shift',
467+
title: 'Cumulative Layout Shift',
468+
description:
469+
'Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more about the Cumulative Layout Shift metric](https://web.dev/cls/).',
470+
score: 1,
471+
scoreDisplayMode: 'numeric',
472+
numericValue: 0.000_350_978_852_728_593_95,
473+
numericUnit: 'unitless',
474+
displayValue: '0',
475+
details: {
476+
type: 'debugdata',
477+
items: [
478+
{
479+
cumulativeLayoutShiftMainFrame: 0.000_350_978_852_728_593_95,
480+
},
481+
],
482+
},
483+
},
484+
]);
485+
486+
// @TODO add check that cliui.logger is called. Resolve TODO after PR #487 is merged.
487+
488+
expect(outputs[0]?.details).toBeUndefined();
489+
});
490+
491+
it('should inform that filmstrip type is not supported yet', () => {
492+
const outputs = toAuditOutputs([
493+
{
494+
id: 'screenshot-thumbnails',
495+
title: 'Screenshot Thumbnails',
496+
description: 'This is what the load of your site looked like.',
497+
score: null,
498+
scoreDisplayMode: 'informative',
499+
details: {
500+
type: 'filmstrip',
501+
scale: 3000,
502+
items: [
503+
{
504+
timing: 375,
505+
timestamp: 106_245_424_545,
506+
data: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwY...',
507+
},
508+
],
509+
},
510+
},
511+
]);
512+
513+
expect(outputs[0]?.details).toBeUndefined();
514+
});
515+
516+
it('should inform that screenshot type is not supported yet', () => {
517+
const outputs = toAuditOutputs([
518+
{
519+
id: 'final-screenshot',
520+
title: 'Final Screenshot',
521+
description: 'The last screenshot captured of the pageload.',
522+
score: null,
523+
scoreDisplayMode: 'informative',
524+
details: {
525+
type: 'screenshot',
526+
timing: 541,
527+
timestamp: 106_245_590_644,
528+
data: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//2Q==',
529+
},
530+
},
531+
]);
532+
533+
expect(outputs[0]?.details).toBeUndefined();
534+
});
535+
536+
it('should inform that treemap-data type is not supported yet', () => {
537+
const outputs = toAuditOutputs([
538+
{
539+
id: 'script-treemap-data',
540+
title: 'Script Treemap Data',
541+
description: 'Used for treemap app',
542+
score: null,
543+
scoreDisplayMode: 'informative',
544+
details: {
545+
type: 'treemap-data',
546+
nodes: [],
547+
},
548+
},
549+
]);
550+
551+
expect(outputs[0]?.details).toBeUndefined();
552+
});
553+
554+
it('should inform that criticalrequestchain type is not supported yet', () => {
555+
const outputs = toAuditOutputs([
556+
{
557+
id: 'critical-request-chains',
558+
title: 'Avoid chaining critical requests',
559+
description:
560+
'The Critical Request Chains below show you what resources are loaded with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load. [Learn how to avoid chaining critical requests](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains/).',
561+
score: null,
562+
scoreDisplayMode: 'notApplicable',
563+
displayValue: '',
564+
details: {
565+
type: 'criticalrequestchain',
566+
chains: {
567+
EED301D300C9A7B634A444E0C6019FC1: {
568+
request: {
569+
url: 'https://example.com/',
570+
startTime: 106_245.050_727,
571+
endTime: 106_245.559_225,
572+
responseReceivedTime: 106_245.559_001,
573+
transferSize: 849,
574+
},
575+
},
576+
},
577+
longestChain: {
578+
duration: 508.498_000_010_848_05,
579+
length: 1,
580+
transferSize: 849,
581+
},
582+
},
583+
},
584+
]);
585+
586+
expect(outputs[0]?.details).toBeUndefined();
587+
});
588+
});

0 commit comments

Comments
 (0)