Skip to content

Commit 5141a00

Browse files
committed
refactor(utils): check scoring prerequisites
1 parent e8d2024 commit 5141a00

3 files changed

Lines changed: 74 additions & 38 deletions

File tree

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

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,6 @@ export type ScoredReport = Omit<Report, 'plugins' | 'categories'> & {
2929
categories: ScoredCategoryConfig[];
3030
};
3131

32-
export function calculateScore<T extends { weight: number }>(
33-
refs: T[],
34-
scoreFn: (ref: T) => number,
35-
): number {
36-
const { numerator, denominator } = refs.reduce(
37-
(acc, ref) => {
38-
const score = scoreFn(ref);
39-
return {
40-
numerator: acc.numerator + score * ref.weight,
41-
denominator: acc.denominator + ref.weight,
42-
};
43-
},
44-
{ numerator: 0, denominator: 0 },
45-
);
46-
// No division by 0, otherwise we produce NaN
47-
// This can be caused by:
48-
// - empty category refs
49-
// - categories with refs only containing weight of `0`
50-
// both should get caught when validating the model
51-
if (!numerator && !denominator) {
52-
throw new Error(
53-
'0 division for score. This can be caused by refs only weighted with 0 or empty refs',
54-
);
55-
}
56-
return numerator / denominator;
57-
}
58-
5932
// eslint-disable-next-line max-lines-per-function
6033
export function scoreReport(report: Report): ScoredReport {
6134
const allScoredAuditsAndGroups = new Map<
@@ -119,3 +92,47 @@ export function scoreReport(report: Report): ScoredReport {
11992
categories: scoredCategories,
12093
};
12194
}
95+
96+
export function calculateScore<T extends { weight: number }>(
97+
refs: T[],
98+
scoreFn: (ref: T) => number,
99+
): number {
100+
const validatedRefs = parseScoringParameters(refs, scoreFn);
101+
const { numerator, denominator } = validatedRefs.reduce(
102+
(acc, ref) => ({
103+
numerator: acc.numerator + ref.score * ref.weight,
104+
denominator: acc.denominator + ref.weight,
105+
}),
106+
{ numerator: 0, denominator: 0 },
107+
);
108+
109+
return numerator / denominator;
110+
}
111+
112+
function parseScoringParameters<T extends { weight: number }>(
113+
refs: T[],
114+
scoreFn: (ref: T) => number,
115+
): { weight: number; score: number }[] {
116+
if (refs.length === 0) {
117+
throw new Error('Reference array cannot be empty.');
118+
}
119+
120+
if (refs.some(ref => ref.weight < 0)) {
121+
throw new Error('Weight cannot be negative.');
122+
}
123+
124+
if (refs.every(ref => ref.weight === 0)) {
125+
throw new Error('All references cannot have zero weight.');
126+
}
127+
128+
const scoredRefs = refs.map(ref => ({
129+
weight: ref.weight,
130+
score: scoreFn(ref),
131+
}));
132+
133+
if (scoredRefs.some(ref => ref.score < 0 || ref.score > 1)) {
134+
throw new Error('All scores must be in range 0-1.');
135+
}
136+
137+
return scoredRefs;
138+
}

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,16 @@ describe('calculateScore', () => {
5656
it('should throw for an empty reference array', () => {
5757
expect(() =>
5858
calculateScore<{ weight: number }>([], ref => ref.weight),
59-
).toThrow('0 division for score');
59+
).toThrow('Reference array cannot be empty.');
60+
});
61+
62+
it('should throw negative weight', () => {
63+
expect(() =>
64+
calculateScore(
65+
[{ slug: 'first-contentful-paint', weight: -1, score: 0.5 }],
66+
ref => ref.score,
67+
),
68+
).toThrow('Weight cannot be negative.');
6069
});
6170

6271
it('should throw for a reference array full of zero weights', () => {
@@ -66,9 +75,27 @@ describe('calculateScore', () => {
6675
{ slug: 'first-contentful-paint', weight: 0, score: 0 },
6776
{ slug: 'cumulative-layout-shift', weight: 0, score: 1 },
6877
],
69-
ref => ref.weight,
78+
ref => ref.score,
79+
),
80+
).toThrow('All references cannot have zero weight.');
81+
});
82+
83+
it('should throw for a negative score', () => {
84+
expect(() =>
85+
calculateScore(
86+
[{ slug: 'first-contentful-paint', weight: 1, score: -0.8 }],
87+
ref => ref.score,
88+
),
89+
).toThrow('All scores must be in range 0-1.');
90+
});
91+
92+
it('should throw for score above 1', () => {
93+
expect(() =>
94+
calculateScore(
95+
[{ slug: 'first-contentful-paint', weight: 1, score: 2 }],
96+
ref => ref.score,
7097
),
71-
).toThrow('0 division for score');
98+
).toThrow('All scores must be in range 0-1.');
7299
});
73100
});
74101

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,15 @@ describe('compareIssueSeverity', () => {
7070

7171
describe('formatReportScore', () => {
7272
it.each([
73-
[Number.NaN, 'NaN'],
74-
[Number.POSITIVE_INFINITY, 'Infinity'],
75-
[Number.NEGATIVE_INFINITY, '-Infinity'],
76-
[-1, '-100'],
77-
[-0.1, '-10'],
7873
[0, '0'],
7974
[0.0049, '0'],
8075
[0.005, '1'],
8176
[0.01, '1'],
8277
[0.123, '12'],
83-
[0.245, '25'],
84-
[0.2449, '24'],
8578
[0.99, '99'],
8679
[0.994, '99'],
8780
[0.995, '100'],
8881
[1, '100'],
89-
[1.1, '110'],
9082
] satisfies readonly [number, string][])(
9183
"should format a score of %d as '%s'",
9284
(score, expected) => {

0 commit comments

Comments
 (0)