Skip to content

Commit 75dc8aa

Browse files
committed
feat(models): create reportsDiff schema
1 parent 09a7ab0 commit 75dc8aa

File tree

4 files changed

+215
-11
lines changed

4 files changed

+215
-11
lines changed

packages/models/src/lib/audit-output.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import { z } from 'zod';
2-
import { nonnegativeIntSchema, slugSchema } from './implementation/schemas';
2+
import {
3+
nonnegativeIntSchema,
4+
scoreSchema,
5+
slugSchema,
6+
} from './implementation/schemas';
37
import { errorItems, hasDuplicateStrings } from './implementation/utils';
48
import { issueSchema } from './issue';
59

10+
export const auditValueSchema =
11+
nonnegativeIntSchema.describe('Raw numeric value');
12+
export const auditDisplayValueSchema = z
13+
.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" })
14+
.optional();
15+
616
export const auditDetailsSchema = z.object(
717
{
818
issues: z.array(issueSchema, { description: 'List of findings' }),
@@ -14,16 +24,9 @@ export type AuditDetails = z.infer<typeof auditDetailsSchema>;
1424
export const auditOutputSchema = z.object(
1525
{
1626
slug: slugSchema.describe('Reference to audit'),
17-
displayValue: z
18-
.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" })
19-
.optional(),
20-
value: nonnegativeIntSchema.describe('Raw numeric value'),
21-
score: z
22-
.number({
23-
description: 'Value between 0 and 1',
24-
})
25-
.min(0)
26-
.max(1),
27+
displayValue: auditDisplayValueSchema,
28+
value: auditValueSchema,
29+
score: scoreSchema,
2730
details: auditDetailsSchema.optional(),
2831
},
2932
{ description: 'Audit information' },

packages/models/src/lib/implementation/schemas.ts

+8
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export const titleSchema = z
5656
.string({ description: 'Descriptive name' })
5757
.max(MAX_TITLE_LENGTH);
5858

59+
/** Schema for score of audit, category or group */
60+
export const scoreSchema = z
61+
.number({
62+
description: 'Value between 0 and 1',
63+
})
64+
.min(0)
65+
.max(1);
66+
5967
/**
6068
* Used for categories, plugins and audits
6169
* @param options
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { type ZodTypeAny, z } from 'zod';
2+
import {
3+
auditDisplayValueSchema,
4+
auditOutputSchema,
5+
auditValueSchema,
6+
} from './audit-output';
7+
import { commitSchema } from './commit';
8+
import {
9+
executionMetaSchema,
10+
packageVersionSchema,
11+
scoreSchema,
12+
slugSchema,
13+
titleSchema,
14+
} from './implementation/schemas';
15+
import { pluginMetaSchema } from './plugin-config';
16+
17+
function makeComparisonSchema<T extends ZodTypeAny>(schema: T) {
18+
return z.object({
19+
before: schema,
20+
after: schema,
21+
});
22+
}
23+
24+
const scorableMetaSchema = z.object({ slug: slugSchema, title: titleSchema });
25+
const scorableWithPluginMetaSchema = scorableMetaSchema.merge(
26+
z.object({
27+
plugin: pluginMetaSchema.pick({ slug: true, title: true }),
28+
}),
29+
);
30+
31+
const scorableDiffSchema = scorableMetaSchema.merge(
32+
z.object({
33+
scores: makeComparisonSchema(scoreSchema).merge(
34+
z.object({ diff: z.number().min(-1).max(1) }),
35+
),
36+
}),
37+
);
38+
const scorableWithPluginDiffSchema = scorableDiffSchema.merge(
39+
scorableWithPluginMetaSchema,
40+
);
41+
42+
const categoryDiffSchema = scorableDiffSchema;
43+
const groupDiffSchema = scorableWithPluginDiffSchema;
44+
const auditDiffSchema = scorableWithPluginDiffSchema.merge(
45+
z.object({
46+
values: makeComparisonSchema(auditValueSchema).merge(
47+
z.object({ diff: z.number().int() }),
48+
),
49+
displayValues: makeComparisonSchema(auditDisplayValueSchema),
50+
}),
51+
);
52+
53+
const categoryResultSchema = scorableMetaSchema.merge(
54+
z.object({ score: scoreSchema }),
55+
);
56+
const groupResultSchema = scorableWithPluginMetaSchema.merge(
57+
z.object({ score: scoreSchema }),
58+
);
59+
const auditResultSchema = scorableWithPluginMetaSchema.merge(
60+
auditOutputSchema.pick({ score: true, value: true, displayValue: true }),
61+
);
62+
63+
export const reportsDiffSchema = z
64+
.object({
65+
commits: makeComparisonSchema(commitSchema).nullable(),
66+
categories: z.object({
67+
changed: z.array(categoryDiffSchema),
68+
unchanged: z.array(categoryResultSchema),
69+
added: z.array(categoryResultSchema),
70+
removed: z.array(categoryResultSchema),
71+
}),
72+
groups: z.object({
73+
changed: z.array(groupDiffSchema),
74+
unchanged: z.array(groupResultSchema),
75+
added: z.array(groupResultSchema),
76+
removed: z.array(groupResultSchema),
77+
}),
78+
audits: z.object({
79+
changed: z.array(auditDiffSchema),
80+
unchanged: z.array(auditResultSchema),
81+
added: z.array(auditResultSchema),
82+
removed: z.array(auditResultSchema),
83+
}),
84+
})
85+
.merge(
86+
packageVersionSchema({
87+
versionDescription: 'NPM version of the CLI',
88+
required: true,
89+
}),
90+
)
91+
.merge(
92+
executionMetaSchema({
93+
descriptionDate: 'Start date and time of the compare run',
94+
descriptionDuration: 'Duration of the compare run in ms',
95+
}),
96+
);
97+
98+
export type ReportsDiff = z.infer<typeof reportsDiffSchema>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { type ReportsDiff, reportsDiffSchema } from './reports-diff';
2+
3+
describe('reportsDiffSchema', () => {
4+
it('should parse valid reports diff', () => {
5+
expect(() =>
6+
reportsDiffSchema.parse({
7+
commits: {
8+
before: {
9+
hash: 'abcdef0123456789abcdef0123456789abcdef01',
10+
message: 'Do stuff',
11+
author: 'John Doe',
12+
date: new Date('2023-03-07T23:00:00+01:00'),
13+
},
14+
after: {
15+
hash: '0123456789abcdef0123456789abcdef01234567',
16+
message: 'Fix stuff',
17+
author: 'Jane Doe',
18+
date: new Date(),
19+
},
20+
},
21+
date: new Date().toISOString(),
22+
duration: 42,
23+
packageName: '@code-pushup/core',
24+
version: '1.2.3',
25+
categories: {
26+
changed: [
27+
{
28+
slug: 'perf',
29+
title: 'Performance',
30+
scores: { before: 0.7, after: 0.66, diff: -0.04 },
31+
},
32+
],
33+
unchanged: [{ slug: 'a11y', title: 'Accessibility', score: 1 }],
34+
added: [],
35+
removed: [],
36+
},
37+
groups: {
38+
changed: [],
39+
unchanged: [],
40+
added: [],
41+
removed: [
42+
{
43+
slug: 'problems',
44+
title: 'Problems',
45+
plugin: { slug: 'eslint', title: 'ESLint' },
46+
score: 0.8,
47+
},
48+
],
49+
},
50+
audits: {
51+
changed: [
52+
{
53+
slug: 'lcp',
54+
title: 'Largest Contentful Paint',
55+
plugin: { slug: 'lighthouse', title: 'Lighthouse' },
56+
scores: {
57+
before: 0.9,
58+
after: 0.7,
59+
diff: -0.2,
60+
},
61+
values: {
62+
before: 1810,
63+
after: 1920,
64+
diff: 110,
65+
},
66+
displayValues: {
67+
before: '1.8 s',
68+
after: '1.9 s',
69+
},
70+
},
71+
],
72+
unchanged: [
73+
{
74+
slug: 'image-alt',
75+
title: 'Image elements have `[alt]` attributes',
76+
plugin: { slug: 'lighthouse', title: 'Lighthouse' },
77+
score: 1,
78+
value: 0,
79+
},
80+
],
81+
added: [
82+
{
83+
slug: 'document-title',
84+
title: 'Document has a `<title>` element',
85+
plugin: { slug: 'lighthouse', title: 'Lighthouse' },
86+
score: 1,
87+
value: 0,
88+
},
89+
],
90+
removed: [],
91+
},
92+
} satisfies ReportsDiff),
93+
).not.toThrow();
94+
});
95+
});

0 commit comments

Comments
 (0)