Skip to content

Commit d45eac4

Browse files
authored
feat(plugin-lighthouse): add onlyAudits logic (#472)
1 parent 20d4f48 commit d45eac4

File tree

10 files changed

+288
-126
lines changed

10 files changed

+288
-126
lines changed

examples/plugins/src/lighthouse/src/lighthouse.plugin.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import {
66
PluginConfig,
77
RunnerConfig,
88
} from '@code-pushup/models';
9-
import { ensureDirectoryExists, toArray } from '@code-pushup/utils';
9+
import {
10+
ensureDirectoryExists,
11+
filterAuditsBySlug,
12+
filterGroupsByAuditSlug,
13+
toArray,
14+
} from '@code-pushup/utils';
1015
import {
1116
LIGHTHOUSE_OUTPUT_FILE_DEFAULT,
1217
PLUGIN_SLUG,
1318
audits,
1419
categoryCorePerfGroup,
1520
} from './constants';
1621
import { LighthouseCliOptions, PluginOptions } from './types';
17-
import {
18-
filterBySlug,
19-
filterRefsBySlug,
20-
getLighthouseCliArguments,
21-
lhrDetailsToIssueDetails,
22-
} from './utils';
22+
import { getLighthouseCliArguments, lhrDetailsToIssueDetails } from './utils';
2323

2424
/**
2525
* @example
@@ -74,8 +74,8 @@ export async function create(options: PluginOptions) {
7474
onlyCategories: ['performance'],
7575
headless,
7676
}),
77-
audits: filterBySlug(audits, onlyAudits),
78-
groups: [filterRefsBySlug(categoryCorePerfGroup, onlyAudits)],
77+
audits: filterAuditsBySlug(audits, onlyAudits),
78+
groups: filterGroupsByAuditSlug([categoryCorePerfGroup], onlyAudits),
7979
} satisfies PluginConfig;
8080
}
8181

examples/plugins/src/lighthouse/src/utils.ts

-41
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,6 @@ import { objectToCliArgs, toArray } from '@code-pushup/utils';
44
import { LIGHTHOUSE_REPORT_NAME } from './constants';
55
import type { LighthouseCliOptions } from './types';
66

7-
export class AuditsNotImplementedError extends Error {
8-
constructor(list: WithSlug[], auditSlugs: string[]) {
9-
super(
10-
`audits: "${auditSlugs
11-
.filter(slug => !list.some(a => a.slug === slug))
12-
.join(', ')}" not implemented`,
13-
);
14-
}
15-
}
16-
17-
export function filterRefsBySlug<T extends { refs: WithSlug[] }>(
18-
group: T,
19-
auditSlugs: string[],
20-
): T {
21-
if (auditSlugs.length === 0) {
22-
return group;
23-
}
24-
const groupsRefs =
25-
auditSlugs.length === 0 ? group.refs : filterBySlug(group.refs, auditSlugs);
26-
27-
return {
28-
...group,
29-
refs: groupsRefs,
30-
};
31-
}
32-
export type WithSlug = { slug: string };
33-
34-
export function filterBySlug<T extends WithSlug>(
35-
list: T[],
36-
auditSlugs: string[],
37-
): T[] {
38-
if (auditSlugs.length === 0) {
39-
return list;
40-
}
41-
if (auditSlugs.some(slug => !list.some(wS => wS.slug === slug))) {
42-
throw new AuditsNotImplementedError(list, auditSlugs);
43-
}
44-
45-
return list.filter(({ slug }) => auditSlugs.includes(slug));
46-
}
47-
487
export function getLighthouseCliArguments(
498
options: LighthouseCliOptions,
509
): string[] {

examples/plugins/src/lighthouse/src/utils.unit.test.ts

+1-68
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22
import { LIGHTHOUSE_URL } from '../mock/constants';
3-
import {
4-
AuditsNotImplementedError,
5-
WithSlug,
6-
filterBySlug,
7-
filterRefsBySlug,
8-
getLighthouseCliArguments,
9-
} from './utils';
10-
11-
describe('filterBySlug', () => {
12-
const list: WithSlug[] = [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }];
13-
const a = list[0] as WithSlug;
14-
it.each<[string, WithSlug[], string[], WithSlug[]]>([
15-
['no-filter', list, [], list],
16-
['a-filter', list, ['a'], [a]],
17-
])(
18-
'should filter by slugs for case "%s"',
19-
(_, testList, slugs, expectedOutput) => {
20-
expect(filterBySlug(testList, slugs)).toEqual(expectedOutput);
21-
},
22-
);
23-
it.each<[string, WithSlug[], string[], string[]]>([
24-
['wrong-filter-1', list, ['d'], ['d']],
25-
['wrong-filter-2', list, ['d', 'a'], ['d']],
26-
])(
27-
'should throw for wrong filter case "%s"',
28-
(_, testList, slugs, wrongSlugs) => {
29-
expect(() => filterBySlug(testList, slugs)).toThrow(
30-
new AuditsNotImplementedError(testList, wrongSlugs),
31-
);
32-
},
33-
);
34-
});
35-
36-
describe('filterRefsBySlug', () => {
37-
const group: { refs: WithSlug[] } = {
38-
refs: [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }],
39-
};
40-
const refA = group.refs[0] as WithSlug;
41-
it.each<[string, { refs: WithSlug[] }, string[], { refs: WithSlug[] }]>([
42-
['no-filter', group, [], group],
43-
[
44-
'a-filter',
45-
group,
46-
['a'],
47-
{
48-
...group,
49-
refs: [refA],
50-
},
51-
],
52-
])(
53-
'should filter by slugs for case "%s"',
54-
(_, testGroup, slugs, expectedOutput) => {
55-
expect(filterRefsBySlug(testGroup, slugs)).toEqual(expectedOutput);
56-
},
57-
);
58-
59-
it.each<[string, { refs: WithSlug[] }, string[], string[]]>([
60-
['wrong-filter-1', group, ['d'], ['d']],
61-
['wrong-filter-2', group, ['a', 'd'], ['d']],
62-
])(
63-
'should throw for wrong filter case "%s"',
64-
(_, testGroup, slugs, wrongSlugs) => {
65-
expect(() => filterRefsBySlug(testGroup, slugs)).toThrow(
66-
new AuditsNotImplementedError(testGroup.refs, wrongSlugs),
67-
);
68-
},
69-
);
70-
});
3+
import { getLighthouseCliArguments } from './utils';
714

725
describe('getLighthouseCliArguments', () => {
736
it('should parse valid options', () => {

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

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { AuditOutputs, PluginConfig } from '@code-pushup/models';
1+
import { Audit, AuditOutputs, Group, PluginConfig } from '@code-pushup/models';
2+
import {
3+
filterAuditsBySlug,
4+
filterGroupsByAuditSlug,
5+
} from '@code-pushup/utils';
26
import { AUDITS, GROUPS, LIGHTHOUSE_PLUGIN_SLUG } from './constants';
7+
import { validateOnlyAudits } from './utils';
38

49
export type LighthousePluginOptions = {
510
url: string;
@@ -10,16 +15,23 @@ export type LighthousePluginOptions = {
1015
userDataDir?: string;
1116
};
1217

13-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14-
export function lighthousePlugin(_: LighthousePluginOptions): PluginConfig {
18+
export function lighthousePlugin(
19+
options: LighthousePluginOptions,
20+
): PluginConfig {
21+
const { onlyAudits = [] } = options;
22+
23+
validateOnlyAudits(AUDITS, onlyAudits);
24+
const audits: Audit[] = filterAuditsBySlug(AUDITS, onlyAudits);
25+
const groups: Group[] = filterGroupsByAuditSlug(GROUPS, onlyAudits);
26+
1527
return {
1628
slug: LIGHTHOUSE_PLUGIN_SLUG,
1729
title: 'Lighthouse',
1830
icon: 'lighthouse',
19-
audits: AUDITS,
20-
groups: GROUPS,
31+
audits,
32+
groups,
2133
runner: (): AuditOutputs =>
22-
AUDITS.map(audit => ({
34+
audits.map(audit => ({
2335
...audit,
2436
score: 0,
2537
value: 0,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { expect } from 'vitest';
2+
import {
3+
auditSchema,
4+
groupSchema,
5+
pluginConfigSchema,
6+
} from '@code-pushup/models';
7+
import { AUDITS, GROUPS } from './constants';
8+
import { lighthousePlugin } from './lighthouse-plugin';
9+
10+
describe('lighthousePlugin-config-object', () => {
11+
it('should create valid plugin config', () => {
12+
const pluginConfig = lighthousePlugin({
13+
url: 'https://code-pushup-portal.com',
14+
});
15+
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
16+
expect(pluginConfig.audits.length).toBeGreaterThan(0);
17+
expect(pluginConfig.groups?.length).toBeGreaterThan(0);
18+
});
19+
20+
it('should filter audits by onlyAudits string "first-contentful-paint"', () => {
21+
const pluginConfig = lighthousePlugin({
22+
url: 'https://code-pushup-portal.com',
23+
onlyAudits: 'first-contentful-paint',
24+
});
25+
26+
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
27+
28+
expect(pluginConfig.audits[0]).toEqual(
29+
expect.objectContaining({
30+
slug: 'first-contentful-paint',
31+
}),
32+
);
33+
});
34+
35+
it('should filter groups by onlyAudits string "first-contentful-paint"', () => {
36+
const pluginConfig = lighthousePlugin({
37+
url: 'https://code-pushup-portal.com',
38+
onlyAudits: 'first-contentful-paint',
39+
});
40+
41+
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
42+
expect(pluginConfig.groups).toHaveLength(1);
43+
44+
const refs = pluginConfig.groups?.[0]?.refs;
45+
expect(refs).toHaveLength(1);
46+
47+
expect(refs).toEqual(
48+
expect.arrayContaining([
49+
expect.objectContaining({
50+
slug: 'first-contentful-paint',
51+
}),
52+
]),
53+
);
54+
});
55+
});
56+
57+
describe('constants', () => {
58+
it.each(AUDITS.map(a => [a.slug, a]))(
59+
'should parse audit "%s" correctly',
60+
(slug, audit) => {
61+
expect(() => auditSchema.parse(audit)).not.toThrow();
62+
expect(audit.slug).toEqual(slug);
63+
},
64+
);
65+
66+
it.each(GROUPS.map(g => [g.slug, g]))(
67+
'should parse group "%s" correctly',
68+
(slug, group) => {
69+
expect(() => groupSchema.parse(group)).not.toThrow();
70+
expect(group.slug).toEqual(slug);
71+
},
72+
);
73+
});

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

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CliFlags } from 'lighthouse';
2-
import { objectToCliArgs } from '@code-pushup/utils';
2+
import { Audit } from '@code-pushup/models';
3+
import { objectToCliArgs, toArray } from '@code-pushup/utils';
34
import { LIGHTHOUSE_REPORT_NAME } from './constants';
45

56
type RefinedLighthouseOption = {
@@ -48,3 +49,22 @@ export function getLighthouseCliArguments(
4849

4950
return objectToCliArgs(argsObj);
5051
}
52+
53+
export class AuditsNotImplementedError extends Error {
54+
constructor(auditSlugs: string[]) {
55+
super(`audits: "${auditSlugs.join(', ')}" not implemented`);
56+
}
57+
}
58+
59+
export function validateOnlyAudits(
60+
audits: Audit[],
61+
onlyAudits: string | string[],
62+
): audits is Audit[] {
63+
const missingAudtis = toArray(onlyAudits).filter(
64+
slug => !audits.some(audit => audit.slug === slug),
65+
);
66+
if (missingAudtis.length > 0) {
67+
throw new AuditsNotImplementedError(missingAudtis);
68+
}
69+
return true;
70+
}

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect } from 'vitest';
2-
import { getLighthouseCliArguments } from './utils';
2+
import {
3+
AuditsNotImplementedError,
4+
getLighthouseCliArguments,
5+
validateOnlyAudits,
6+
} from './utils';
37

48
describe('getLighthouseCliArguments', () => {
59
it('should parse valid options', () => {
@@ -22,3 +26,31 @@ describe('getLighthouseCliArguments', () => {
2226
);
2327
});
2428
});
29+
30+
describe('validateOnlyAudits', () => {
31+
it('should not throw for audit slugs existing in given audits', () => {
32+
expect(
33+
validateOnlyAudits(
34+
[
35+
{ slug: 'a', title: 'A' },
36+
{ slug: 'b', title: 'B' },
37+
{ slug: 'c', title: 'C' },
38+
],
39+
'a',
40+
),
41+
).toBeTruthy();
42+
});
43+
44+
it('should throw if given onlyAudits do not exist', () => {
45+
expect(() =>
46+
validateOnlyAudits(
47+
[
48+
{ slug: 'a', title: 'A' },
49+
{ slug: 'b', title: 'B' },
50+
{ slug: 'c', title: 'C' },
51+
],
52+
'd',
53+
),
54+
).toThrow(new AuditsNotImplementedError(['d']));
55+
});
56+
});

packages/utils/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ export {
7171
} from './lib/transform';
7272
export { verboseUtils } from './lib/verbose-utils';
7373
export { link } from './lib/logging';
74+
export {
75+
filterAuditsBySlug,
76+
filterGroupsByAuditSlug,
77+
} from './lib/filter-by-slug';
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Audit, Group } from '@code-pushup/models';
2+
import { toArray } from './transform';
3+
4+
export function filterGroupsByAuditSlug(
5+
groups: Group[],
6+
auditSlugs: string | string[],
7+
): Group[] {
8+
const slugs = toArray(auditSlugs);
9+
if (slugs.length === 0) {
10+
return groups;
11+
}
12+
return (
13+
groups
14+
.map(group => ({
15+
...group,
16+
refs: filterSlug(group.refs, slugs),
17+
}))
18+
// filter out groups that have no audits includes from onlyAudits (avoid empty groups)
19+
.filter(group => group.refs.length)
20+
);
21+
}
22+
23+
export function filterAuditsBySlug(
24+
list: Audit[],
25+
auditSlugs: string[] | string,
26+
): Audit[] {
27+
const slugs = toArray(auditSlugs);
28+
if (slugs.length === 0) {
29+
return list;
30+
}
31+
return filterSlug(list, slugs);
32+
}
33+
34+
export function filterSlug<T extends { slug: string }>(
35+
refs: T[],
36+
slugOrSlugs: string | string[],
37+
): T[] {
38+
const slugs = toArray(slugOrSlugs);
39+
return refs.filter(({ slug }) => slugs.includes(slug));
40+
}

0 commit comments

Comments
 (0)