Skip to content

Commit

Permalink
feat(plugin-lighthouse): add onlyAudits logic (#472)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed Feb 7, 2024
1 parent 20d4f48 commit d45eac4
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 126 deletions.
18 changes: 9 additions & 9 deletions examples/plugins/src/lighthouse/src/lighthouse.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import {
PluginConfig,
RunnerConfig,
} from '@code-pushup/models';
import { ensureDirectoryExists, toArray } from '@code-pushup/utils';
import {
ensureDirectoryExists,
filterAuditsBySlug,
filterGroupsByAuditSlug,
toArray,
} from '@code-pushup/utils';
import {
LIGHTHOUSE_OUTPUT_FILE_DEFAULT,
PLUGIN_SLUG,
audits,
categoryCorePerfGroup,
} from './constants';
import { LighthouseCliOptions, PluginOptions } from './types';
import {
filterBySlug,
filterRefsBySlug,
getLighthouseCliArguments,
lhrDetailsToIssueDetails,
} from './utils';
import { getLighthouseCliArguments, lhrDetailsToIssueDetails } from './utils';

/**
* @example
Expand Down Expand Up @@ -74,8 +74,8 @@ export async function create(options: PluginOptions) {
onlyCategories: ['performance'],
headless,
}),
audits: filterBySlug(audits, onlyAudits),
groups: [filterRefsBySlug(categoryCorePerfGroup, onlyAudits)],
audits: filterAuditsBySlug(audits, onlyAudits),
groups: filterGroupsByAuditSlug([categoryCorePerfGroup], onlyAudits),
} satisfies PluginConfig;
}

Expand Down
41 changes: 0 additions & 41 deletions examples/plugins/src/lighthouse/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,6 @@ import { objectToCliArgs, toArray } from '@code-pushup/utils';
import { LIGHTHOUSE_REPORT_NAME } from './constants';
import type { LighthouseCliOptions } from './types';

export class AuditsNotImplementedError extends Error {
constructor(list: WithSlug[], auditSlugs: string[]) {
super(
`audits: "${auditSlugs
.filter(slug => !list.some(a => a.slug === slug))
.join(', ')}" not implemented`,
);
}
}

export function filterRefsBySlug<T extends { refs: WithSlug[] }>(
group: T,
auditSlugs: string[],
): T {
if (auditSlugs.length === 0) {
return group;
}
const groupsRefs =
auditSlugs.length === 0 ? group.refs : filterBySlug(group.refs, auditSlugs);

return {
...group,
refs: groupsRefs,
};
}
export type WithSlug = { slug: string };

export function filterBySlug<T extends WithSlug>(
list: T[],
auditSlugs: string[],
): T[] {
if (auditSlugs.length === 0) {
return list;
}
if (auditSlugs.some(slug => !list.some(wS => wS.slug === slug))) {
throw new AuditsNotImplementedError(list, auditSlugs);
}

return list.filter(({ slug }) => auditSlugs.includes(slug));
}

export function getLighthouseCliArguments(
options: LighthouseCliOptions,
): string[] {
Expand Down
69 changes: 1 addition & 68 deletions examples/plugins/src/lighthouse/src/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,6 @@
import { describe, expect, it } from 'vitest';
import { LIGHTHOUSE_URL } from '../mock/constants';
import {
AuditsNotImplementedError,
WithSlug,
filterBySlug,
filterRefsBySlug,
getLighthouseCliArguments,
} from './utils';

describe('filterBySlug', () => {
const list: WithSlug[] = [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }];
const a = list[0] as WithSlug;
it.each<[string, WithSlug[], string[], WithSlug[]]>([
['no-filter', list, [], list],
['a-filter', list, ['a'], [a]],
])(
'should filter by slugs for case "%s"',
(_, testList, slugs, expectedOutput) => {
expect(filterBySlug(testList, slugs)).toEqual(expectedOutput);
},
);
it.each<[string, WithSlug[], string[], string[]]>([
['wrong-filter-1', list, ['d'], ['d']],
['wrong-filter-2', list, ['d', 'a'], ['d']],
])(
'should throw for wrong filter case "%s"',
(_, testList, slugs, wrongSlugs) => {
expect(() => filterBySlug(testList, slugs)).toThrow(
new AuditsNotImplementedError(testList, wrongSlugs),
);
},
);
});

describe('filterRefsBySlug', () => {
const group: { refs: WithSlug[] } = {
refs: [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }],
};
const refA = group.refs[0] as WithSlug;
it.each<[string, { refs: WithSlug[] }, string[], { refs: WithSlug[] }]>([
['no-filter', group, [], group],
[
'a-filter',
group,
['a'],
{
...group,
refs: [refA],
},
],
])(
'should filter by slugs for case "%s"',
(_, testGroup, slugs, expectedOutput) => {
expect(filterRefsBySlug(testGroup, slugs)).toEqual(expectedOutput);
},
);

it.each<[string, { refs: WithSlug[] }, string[], string[]]>([
['wrong-filter-1', group, ['d'], ['d']],
['wrong-filter-2', group, ['a', 'd'], ['d']],
])(
'should throw for wrong filter case "%s"',
(_, testGroup, slugs, wrongSlugs) => {
expect(() => filterRefsBySlug(testGroup, slugs)).toThrow(
new AuditsNotImplementedError(testGroup.refs, wrongSlugs),
);
},
);
});
import { getLighthouseCliArguments } from './utils';

describe('getLighthouseCliArguments', () => {
it('should parse valid options', () => {
Expand Down
24 changes: 18 additions & 6 deletions packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { AuditOutputs, PluginConfig } from '@code-pushup/models';
import { Audit, AuditOutputs, Group, PluginConfig } from '@code-pushup/models';
import {
filterAuditsBySlug,
filterGroupsByAuditSlug,
} from '@code-pushup/utils';
import { AUDITS, GROUPS, LIGHTHOUSE_PLUGIN_SLUG } from './constants';
import { validateOnlyAudits } from './utils';

export type LighthousePluginOptions = {
url: string;
Expand All @@ -10,16 +15,23 @@ export type LighthousePluginOptions = {
userDataDir?: string;
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function lighthousePlugin(_: LighthousePluginOptions): PluginConfig {
export function lighthousePlugin(
options: LighthousePluginOptions,
): PluginConfig {
const { onlyAudits = [] } = options;

validateOnlyAudits(AUDITS, onlyAudits);
const audits: Audit[] = filterAuditsBySlug(AUDITS, onlyAudits);
const groups: Group[] = filterGroupsByAuditSlug(GROUPS, onlyAudits);

return {
slug: LIGHTHOUSE_PLUGIN_SLUG,
title: 'Lighthouse',
icon: 'lighthouse',
audits: AUDITS,
groups: GROUPS,
audits,
groups,
runner: (): AuditOutputs =>
AUDITS.map(audit => ({
audits.map(audit => ({
...audit,
score: 0,
value: 0,
Expand Down
73 changes: 73 additions & 0 deletions packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from 'vitest';
import {
auditSchema,
groupSchema,
pluginConfigSchema,
} from '@code-pushup/models';
import { AUDITS, GROUPS } from './constants';
import { lighthousePlugin } from './lighthouse-plugin';

describe('lighthousePlugin-config-object', () => {
it('should create valid plugin config', () => {
const pluginConfig = lighthousePlugin({
url: 'https://code-pushup-portal.com',
});
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
expect(pluginConfig.audits.length).toBeGreaterThan(0);
expect(pluginConfig.groups?.length).toBeGreaterThan(0);
});

it('should filter audits by onlyAudits string "first-contentful-paint"', () => {
const pluginConfig = lighthousePlugin({
url: 'https://code-pushup-portal.com',
onlyAudits: 'first-contentful-paint',
});

expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();

expect(pluginConfig.audits[0]).toEqual(
expect.objectContaining({
slug: 'first-contentful-paint',
}),
);
});

it('should filter groups by onlyAudits string "first-contentful-paint"', () => {
const pluginConfig = lighthousePlugin({
url: 'https://code-pushup-portal.com',
onlyAudits: 'first-contentful-paint',
});

expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
expect(pluginConfig.groups).toHaveLength(1);

const refs = pluginConfig.groups?.[0]?.refs;
expect(refs).toHaveLength(1);

expect(refs).toEqual(
expect.arrayContaining([
expect.objectContaining({
slug: 'first-contentful-paint',
}),
]),
);
});
});

describe('constants', () => {
it.each(AUDITS.map(a => [a.slug, a]))(
'should parse audit "%s" correctly',
(slug, audit) => {
expect(() => auditSchema.parse(audit)).not.toThrow();
expect(audit.slug).toEqual(slug);
},
);

it.each(GROUPS.map(g => [g.slug, g]))(
'should parse group "%s" correctly',
(slug, group) => {
expect(() => groupSchema.parse(group)).not.toThrow();
expect(group.slug).toEqual(slug);
},
);
});
22 changes: 21 additions & 1 deletion packages/plugin-lighthouse/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CliFlags } from 'lighthouse';
import { objectToCliArgs } from '@code-pushup/utils';
import { Audit } from '@code-pushup/models';
import { objectToCliArgs, toArray } from '@code-pushup/utils';
import { LIGHTHOUSE_REPORT_NAME } from './constants';

type RefinedLighthouseOption = {
Expand Down Expand Up @@ -48,3 +49,22 @@ export function getLighthouseCliArguments(

return objectToCliArgs(argsObj);
}

export class AuditsNotImplementedError extends Error {
constructor(auditSlugs: string[]) {
super(`audits: "${auditSlugs.join(', ')}" not implemented`);
}
}

export function validateOnlyAudits(
audits: Audit[],
onlyAudits: string | string[],
): audits is Audit[] {
const missingAudtis = toArray(onlyAudits).filter(
slug => !audits.some(audit => audit.slug === slug),
);
if (missingAudtis.length > 0) {
throw new AuditsNotImplementedError(missingAudtis);
}
return true;
}
34 changes: 33 additions & 1 deletion packages/plugin-lighthouse/src/lib/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect } from 'vitest';
import { getLighthouseCliArguments } from './utils';
import {
AuditsNotImplementedError,
getLighthouseCliArguments,
validateOnlyAudits,
} from './utils';

describe('getLighthouseCliArguments', () => {
it('should parse valid options', () => {
Expand All @@ -22,3 +26,31 @@ describe('getLighthouseCliArguments', () => {
);
});
});

describe('validateOnlyAudits', () => {
it('should not throw for audit slugs existing in given audits', () => {
expect(
validateOnlyAudits(
[
{ slug: 'a', title: 'A' },
{ slug: 'b', title: 'B' },
{ slug: 'c', title: 'C' },
],
'a',
),
).toBeTruthy();
});

it('should throw if given onlyAudits do not exist', () => {
expect(() =>
validateOnlyAudits(
[
{ slug: 'a', title: 'A' },
{ slug: 'b', title: 'B' },
{ slug: 'c', title: 'C' },
],
'd',
),
).toThrow(new AuditsNotImplementedError(['d']));
});
});
4 changes: 4 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ export {
} from './lib/transform';
export { verboseUtils } from './lib/verbose-utils';
export { link } from './lib/logging';
export {
filterAuditsBySlug,
filterGroupsByAuditSlug,
} from './lib/filter-by-slug';
40 changes: 40 additions & 0 deletions packages/utils/src/lib/filter-by-slug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Audit, Group } from '@code-pushup/models';
import { toArray } from './transform';

export function filterGroupsByAuditSlug(
groups: Group[],
auditSlugs: string | string[],
): Group[] {
const slugs = toArray(auditSlugs);
if (slugs.length === 0) {
return groups;
}
return (
groups
.map(group => ({
...group,
refs: filterSlug(group.refs, slugs),
}))
// filter out groups that have no audits includes from onlyAudits (avoid empty groups)
.filter(group => group.refs.length)
);
}

export function filterAuditsBySlug(
list: Audit[],
auditSlugs: string[] | string,
): Audit[] {
const slugs = toArray(auditSlugs);
if (slugs.length === 0) {
return list;
}
return filterSlug(list, slugs);
}

export function filterSlug<T extends { slug: string }>(
refs: T[],
slugOrSlugs: string | string[],
): T[] {
const slugs = toArray(slugOrSlugs);
return refs.filter(({ slug }) => slugs.includes(slug));
}
Loading

0 comments on commit d45eac4

Please sign in to comment.