Skip to content

Commit 397ba31

Browse files
committed
feat: implement scoreTarget for audits
1 parent 4d54550 commit 397ba31

File tree

5 files changed

+217
-3
lines changed

5 files changed

+217
-3
lines changed

packages/core/src/lib/implementation/execute-plugin.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
groupByStatus,
1515
logMultipleResults,
1616
pluralizeToken,
17+
scoreAuditsWithTarget,
1718
} from '@code-pushup/utils';
1819
import {
1920
executePluginRunner,
@@ -57,6 +58,7 @@ export async function executePlugin(
5758
description,
5859
docsUrl,
5960
groups,
61+
scoreTarget,
6062
...pluginMeta
6163
} = pluginConfig;
6264
const { write: cacheWrite = false, read: cacheRead = false } = cache;
@@ -76,8 +78,13 @@ export async function executePlugin(
7678
});
7779
}
7880

81+
// transform audit scores to 1 when they meet/exceed scoreTarget
82+
const transformedAudits = scoreTarget
83+
? scoreAuditsWithTarget(audits, scoreTarget)
84+
: audits;
85+
7986
// enrich `AuditOutputs` to `AuditReport`
80-
const auditReports: AuditReport[] = audits.map(
87+
const auditReports: AuditReport[] = transformedAudits.map(
8188
(auditOutput: AuditOutput) => ({
8289
...auditOutput,
8390
...(pluginConfigAudits.find(

packages/core/src/lib/implementation/execute-plugin.unit.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,67 @@ describe('executePlugin', () => {
124124
MINIMAL_PLUGIN_CONFIG_MOCK,
125125
);
126126
});
127+
128+
it('should apply a single score target to all audits', async () => {
129+
const pluginConfig: PluginConfig = {
130+
...MINIMAL_PLUGIN_CONFIG_MOCK,
131+
scoreTarget: 0.8,
132+
audits: [
133+
{
134+
slug: 'speed-index',
135+
title: 'Speed Index',
136+
},
137+
{
138+
slug: 'total-blocking-time',
139+
title: 'Total Blocking Time',
140+
},
141+
],
142+
runner: () => [
143+
{ slug: 'speed-index', score: 0.9, value: 1300 },
144+
{ slug: 'total-blocking-time', score: 0.3, value: 600 },
145+
],
146+
};
147+
148+
const result = await executePlugin(pluginConfig, {
149+
persist: { outputDir: '' },
150+
cache: { read: false, write: false },
151+
});
152+
153+
expect(result.audits).toEqual(
154+
expect.arrayContaining([
155+
expect.objectContaining({
156+
slug: 'speed-index',
157+
score: 1,
158+
scoreTarget: 0.8,
159+
}),
160+
expect.objectContaining({
161+
slug: 'total-blocking-time',
162+
score: 0.3,
163+
scoreTarget: 0.8,
164+
}),
165+
]),
166+
);
167+
});
168+
169+
it('should apply per-audit score targets', async () => {
170+
const pluginConfig: PluginConfig = {
171+
...MINIMAL_PLUGIN_CONFIG_MOCK, // returns node-version audit with score 0.3
172+
scoreTarget: {
173+
'node-version': 0.2,
174+
},
175+
};
176+
177+
const result = await executePlugin(pluginConfig, {
178+
persist: { outputDir: '' },
179+
cache: { read: false, write: false },
180+
});
181+
182+
expect(result.audits[0]).toMatchObject({
183+
slug: 'node-version',
184+
score: 1,
185+
scoreTarget: 0.2,
186+
});
187+
});
127188
});
128189

129190
describe('executePlugins', () => {

packages/utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export {
102102
} from './lib/reports/generate-md-reports-diff.js';
103103
export { loadReport } from './lib/reports/load-report.js';
104104
export { logStdoutSummary } from './lib/reports/log-stdout-summary.js';
105-
export { scoreReport } from './lib/reports/scoring.js';
105+
export { scoreReport, scoreAuditsWithTarget } from './lib/reports/scoring.js';
106106
export { sortReport } from './lib/reports/sorting.js';
107107
export type {
108108
ScoredCategoryConfig,

packages/utils/src/lib/reports/scoring.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type {
2+
AuditOutput,
23
AuditReport,
34
CategoryRef,
45
GroupRef,
6+
PluginScoreTarget,
57
Report,
68
} from '@code-pushup/models';
79
import { deepClone } from '../transform.js';
@@ -117,3 +119,37 @@ function parseScoringParameters<T extends { weight: number }>(
117119

118120
return scoredRefs;
119121
}
122+
123+
/**
124+
* Sets audit score to 1 if it meets target.
125+
* @param audit audit output to evaluate
126+
* @param scoreTarget threshold for perfect score (0-1)
127+
* @returns Audit with scoreTarget field
128+
*/
129+
export function scoreAuditWithTarget(
130+
audit: AuditOutput,
131+
scoreTarget: number,
132+
): AuditOutput {
133+
return audit.score >= scoreTarget
134+
? { ...audit, score: 1, scoreTarget }
135+
: { ...audit, scoreTarget };
136+
}
137+
138+
/**
139+
* Sets audit scores to 1 when targets are met.
140+
* @param audits audit outputs from plugin execution
141+
* @param scoreTarget number or { slug: target } mapping
142+
* @returns Transformed audits with scoreTarget field
143+
*/
144+
export function scoreAuditsWithTarget(
145+
audits: AuditOutput[],
146+
scoreTarget: PluginScoreTarget,
147+
): AuditOutput[] {
148+
if (typeof scoreTarget === 'number') {
149+
return audits.map(audit => scoreAuditWithTarget(audit, scoreTarget));
150+
}
151+
return audits.map(audit => {
152+
const target = scoreTarget?.[audit.slug];
153+
return target == null ? audit : scoreAuditWithTarget(audit, target);
154+
});
155+
}

packages/utils/src/lib/reports/scoring.unit.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, expect } from 'vitest';
22
import { REPORT_MOCK } from '@code-pushup/test-utils';
3-
import { calculateScore, scoreReport } from './scoring.js';
3+
import {
4+
calculateScore,
5+
scoreAuditWithTarget,
6+
scoreAuditsWithTarget,
7+
scoreReport,
8+
} from './scoring.js';
49

510
describe('calculateScore', () => {
611
it('should calculate the same score for one reference', () => {
@@ -136,3 +141,108 @@ describe('scoreReport', () => {
136141
);
137142
});
138143
});
144+
145+
describe('scoreAuditWithTarget', () => {
146+
it('should add scoreTarget and increase an audit score to 1 when the target is reached', () => {
147+
expect(
148+
scoreAuditWithTarget(
149+
{ slug: 'speed-index', score: 0.9, value: 1300 },
150+
0.8,
151+
),
152+
).toEqual({
153+
slug: 'speed-index',
154+
score: 1,
155+
value: 1300,
156+
scoreTarget: 0.8,
157+
});
158+
});
159+
160+
it('should only add scoreTarget when the target is not reached', () => {
161+
expect(
162+
scoreAuditWithTarget(
163+
{ slug: 'largest-contentful-paint', score: 0.6, value: 3000 },
164+
0.8,
165+
),
166+
).toEqual({
167+
slug: 'largest-contentful-paint',
168+
score: 0.6,
169+
value: 3000,
170+
scoreTarget: 0.8,
171+
});
172+
});
173+
});
174+
175+
describe('scoreAuditsWithTarget', () => {
176+
it('should apply a single score target to all audits', () => {
177+
const audits = [
178+
{ slug: 'first-contentful-paint', score: 0.8, value: 1200 },
179+
{ slug: 'largest-contentful-paint', score: 0.6, value: 3000 },
180+
{ slug: 'speed-index', score: 0.9, value: 1300 },
181+
];
182+
183+
expect(scoreAuditsWithTarget(audits, 0.75)).toEqual([
184+
{
185+
slug: 'first-contentful-paint',
186+
score: 1,
187+
value: 1200,
188+
scoreTarget: 0.75,
189+
},
190+
{
191+
slug: 'largest-contentful-paint',
192+
score: 0.6,
193+
value: 3000,
194+
scoreTarget: 0.75,
195+
},
196+
{ slug: 'speed-index', score: 1, value: 1300, scoreTarget: 0.75 },
197+
]);
198+
});
199+
200+
it('should apply per-audit score targets', () => {
201+
const audits = [
202+
{ slug: 'first-contentful-paint', score: 0.8, value: 1200 },
203+
{ slug: 'largest-contentful-paint', score: 0.6, value: 3000 },
204+
{ slug: 'speed-index', score: 0.9, value: 1300 },
205+
];
206+
207+
expect(
208+
scoreAuditsWithTarget(audits, {
209+
'first-contentful-paint': 0.85,
210+
'largest-contentful-paint': 0.5,
211+
}),
212+
).toEqual([
213+
{
214+
slug: 'first-contentful-paint',
215+
score: 0.8,
216+
value: 1200,
217+
scoreTarget: 0.85,
218+
},
219+
{
220+
slug: 'largest-contentful-paint',
221+
score: 1,
222+
value: 3000,
223+
scoreTarget: 0.5,
224+
},
225+
{ slug: 'speed-index', score: 0.9, value: 1300 },
226+
]);
227+
});
228+
229+
it('should set an audit score to 1 when the original score equals the target', () => {
230+
const audits = [{ slug: 'speed-index', score: 0.9, value: 1300 }];
231+
232+
expect(scoreAuditsWithTarget(audits, 0.9)).toEqual([
233+
{ slug: 'speed-index', score: 1, value: 1300, scoreTarget: 0.9 },
234+
]);
235+
});
236+
237+
it('should handle an empty audits array', () => {
238+
expect(scoreAuditsWithTarget([], 0.8)).toEqual([]);
239+
});
240+
241+
it('should handle an empty score target record', () => {
242+
const audits = [{ slug: 'speed-index', score: 0.9, value: 1300 }];
243+
244+
expect(scoreAuditsWithTarget(audits, {})).toEqual([
245+
{ slug: 'speed-index', score: 0.9, value: 1300 },
246+
]);
247+
});
248+
});

0 commit comments

Comments
 (0)