Skip to content

Commit 10ee12e

Browse files
feat(utils): add audits sorting for reports (#302)
Added audits sorting for both reports, md and stdout, added sorting of audits at the categories and groups. Closes #210
1 parent ee21143 commit 10ee12e

7 files changed

+497
-281
lines changed

packages/utils/src/lib/__snapshots__/report-to-md.integration.test.ts.snap

+165-168
Large diffs are not rendered by default.

packages/utils/src/lib/__snapshots__/report-to-stdout.integration.test.ts.snap

+43-43
Original file line numberDiff line numberDiff line change
@@ -6,71 +6,71 @@ exports[`report-to-stdout > should contain all sections when using the fixture r
66
77
ESLint audits
88
9-
● Disallow assignment operators in conditional expressions 0
10-
● Disallow reassigning \`const\` variables 0
11-
● Disallow the use of \`debugger\` 0
12-
● Disallow invalid regular expression strings in \`RegExp\` 0
13-
constructors
14-
● Disallow the use of undeclared variables unless mentioned in 0
15-
\`/*global */\` comments
16-
● Disallow loops with a body that allows only one iteration 0
17-
● Disallow negating the left operand of relational operators 0
18-
● Disallow use of optional chaining in contexts where the 0
19-
\`undefined\` value is not allowed
20-
● Disallow unused variables 1 warning
21-
● Require calls to \`isNaN()\` when checking for \`NaN\` 0
22-
● Enforce comparing \`typeof\` expressions against valid strings 0
23-
● Require braces around arrow function bodies 1 warning
24-
● Enforce camelcase naming convention 0
25-
● Enforce consistent brace style for all control statements 0
26-
● Require the use of \`===\` and \`!==\` 1 warning
27-
● Enforce a maximum number of lines of code in a function 1 warning
28-
● Enforce a maximum number of lines per file 0
9+
● Disallow missing props validation in a React component definition 6 warnings
2910
● Disallow variable declarations from shadowing variables declared 3 warnings
3011
in the outer scope
31-
● Require \`let\` or \`const\` instead of \`var\` 0
3212
● Require or disallow method and property shorthand syntax for 3 warnings
3313
object literals
34-
● Require using arrow functions for callbacks 0
35-
● Require \`const\` declarations for variables that are never 1 warning
36-
reassigned after declared
37-
● Disallow using Object.assign with an object literal as the first 0
38-
argument and prefer the use of object spread instead
39-
● Require or disallow \\"Yoda\\" conditions 0
40-
● Disallow missing \`key\` props in iterators/collection literals 1 warning
41-
● Disallow missing props validation in a React component definition 6 warnings
42-
● Disallow missing React when using JSX 0
43-
● enforces the Rules of Hooks 0
4414
● verifies the list of dependencies for Hooks like useEffect and 2 warnings
4515
similar
46-
● Disallow missing displayName in a React component definition 0
16+
● Disallow missing \`key\` props in iterators/collection literals 1 warning
17+
● Disallow unused variables 1 warning
18+
● Enforce a maximum number of lines of code in a function 1 warning
19+
● Require \`const\` declarations for variables that are never 1 warning
20+
reassigned after declared
21+
● Require braces around arrow function bodies 1 warning
22+
● Require the use of \`===\` and \`!==\` 1 warning
23+
● Disallow \`target=\\"_blank\\"\` attribute without \`rel=\\"noreferrer\\"\` 0
24+
● Disallow assignment operators in conditional expressions 0
4725
● Disallow comments from being inserted as text nodes 0
26+
● Disallow direct mutation of this.state 0
4827
● Disallow duplicate properties in JSX 0
49-
● Disallow \`target=\\"_blank\\"\` attribute without \`rel=\\"noreferrer\\"\` 0
50-
● Disallow undeclared variables in JSX 0
51-
● Disallow React to be incorrectly marked as unused 0
52-
● Disallow variables used in JSX to be incorrectly marked as unused 0
28+
● Disallow invalid regular expression strings in \`RegExp\` 0
29+
constructors
30+
● Disallow loops with a body that allows only one iteration 0
31+
● Disallow missing displayName in a React component definition 0
32+
● Disallow missing React when using JSX 0
33+
● Disallow negating the left operand of relational operators 0
5334
● Disallow passing of children as props 0
54-
● Disallow when a DOM element is using both children and 0
55-
dangerouslySetInnerHTML
35+
● Disallow React to be incorrectly marked as unused 0
36+
● Disallow reassigning \`const\` variables 0
37+
● Disallow the use of \`debugger\` 0
38+
● Disallow the use of undeclared variables unless mentioned in 0
39+
\`/*global */\` comments
40+
● Disallow undeclared variables in JSX 0
41+
● Disallow unescaped HTML entities from appearing in markup 0
5642
● Disallow usage of deprecated methods 0
57-
● Disallow direct mutation of this.state 0
5843
● Disallow usage of findDOMNode 0
5944
● Disallow usage of isMounted 0
6045
● Disallow usage of the return value of ReactDOM.render 0
61-
● Disallow using string references 0
62-
● Disallow unescaped HTML entities from appearing in markup 0
6346
● Disallow usage of unknown DOM property 0
47+
● Disallow use of optional chaining in contexts where the 0
48+
\`undefined\` value is not allowed
49+
● Disallow using Object.assign with an object literal as the first 0
50+
argument and prefer the use of object spread instead
51+
● Disallow using string references 0
52+
● Disallow variables used in JSX to be incorrectly marked as unused 0
53+
● Disallow when a DOM element is using both children and 0
54+
dangerouslySetInnerHTML
55+
● Enforce a maximum number of lines per file 0
56+
● Enforce camelcase naming convention 0
57+
● Enforce comparing \`typeof\` expressions against valid strings 0
58+
● Enforce consistent brace style for all control statements 0
6459
● Enforce ES5 or ES6 class for returning value in render function 0
60+
● enforces the Rules of Hooks 0
61+
● Require \`let\` or \`const\` instead of \`var\` 0
62+
● Require calls to \`isNaN()\` when checking for \`NaN\` 0
63+
● Require or disallow \\"Yoda\\" conditions 0
64+
● Require using arrow functions for callbacks 0
6565
6666
6767
Lighthouse audits
6868
6969
● First Contentful Paint 1.2 s
7070
● Largest Contentful Paint 1.5 s
71-
● Total Blocking Time 0 ms
72-
● Cumulative Layout Shift 0
7371
● Speed Index 1.2 s
72+
● Cumulative Layout Shift 0
73+
● Total Blocking Time 0 ms
7474
7575
7676
Categories

packages/utils/src/lib/report-to-md.ts

+67-67
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
22
AuditReport,
33
CategoryConfig,
4+
CategoryRef,
45
Issue,
5-
PluginReport,
66
} from '@code-pushup/models';
77
import { CommitData } from './git';
88
import {
@@ -24,15 +24,24 @@ import {
2424
detailsTableHeaders,
2525
formatDuration,
2626
formatReportScore,
27+
getAuditByRef,
28+
getGroupWithAudits,
29+
getPluginNameFromSlug,
2730
getRoundScoreMarker,
2831
getSeverityIcon,
2932
getSquaredScoreMarker,
3033
pluginMetaTableHeaders,
3134
reportHeadlineText,
3235
reportMetaTableHeaders,
3336
reportOverviewTableHeaders,
37+
sortAudits,
38+
sortCategoryAudits,
3439
} from './report';
35-
import { EnrichedScoredAuditGroup, ScoredReport } from './scoring';
40+
import {
41+
EnrichedScoredAuditGroupWithAudits,
42+
ScoredReport,
43+
WeighedAuditReport,
44+
} from './scoring';
3645
import { slugify } from './transformation';
3746

3847
export function reportToMd(
@@ -87,14 +96,37 @@ function reportToCategoriesSection(report: ScoredReport): string {
8796
)} Score: ${style(formatReportScore(category.score))}`;
8897
const categoryDocs = getDocsAndDescription(category);
8998

90-
const refs = category.refs.reduce((acc, ref) => {
91-
if (ref.type === 'group') {
92-
acc += groupRefItemToCategorySection(ref.slug, ref.plugin, plugins);
93-
} else {
94-
acc += auditRefItemToCategorySection(ref.slug, ref.plugin, plugins);
95-
}
96-
return acc;
97-
}, '');
99+
const auditsAndGroups = category.refs.reduce(
100+
(
101+
acc: {
102+
audits: WeighedAuditReport[];
103+
groups: EnrichedScoredAuditGroupWithAudits[];
104+
},
105+
ref: CategoryRef,
106+
) => ({
107+
...acc,
108+
...(ref.type === 'group'
109+
? {
110+
groups: [
111+
...acc.groups,
112+
getGroupWithAudits(ref.slug, ref.plugin, plugins),
113+
],
114+
}
115+
: {
116+
audits: [...acc.audits, getAuditByRef(ref, plugins)],
117+
}),
118+
}),
119+
{ groups: [], audits: [] },
120+
);
121+
122+
const audits = auditsAndGroups.audits
123+
.sort(sortCategoryAudits)
124+
.map(audit => auditItemToCategorySection(audit, plugins))
125+
.join(NEW_LINE);
126+
127+
const groups = auditsAndGroups.groups
128+
.map(group => groupItemToCategorySection(group, plugins))
129+
.join('');
98130

99131
return (
100132
acc +
@@ -105,73 +137,43 @@ function reportToCategoriesSection(report: ScoredReport): string {
105137
categoryDocs +
106138
categoryScore +
107139
NEW_LINE +
140+
groups +
108141
NEW_LINE +
109-
refs
142+
audits
110143
);
111144
}, '');
112145

113146
return h2('🏷 Categories') + NEW_LINE + categoryDetails;
114147
}
115148

116-
function auditRefItemToCategorySection(
117-
refSlug: string,
118-
refPlugin: string,
149+
function auditItemToCategorySection(
150+
audit: WeighedAuditReport,
119151
plugins: ScoredReport['plugins'],
120152
): string {
121-
const plugin = plugins.find(({ slug }) => slug === refPlugin) as PluginReport;
122-
const pluginAudit = plugin?.audits.find(({ slug }) => slug === refSlug);
123-
124-
if (!pluginAudit) {
125-
throwIsNotPresentError(`Audit ${refSlug}`, plugin?.slug);
126-
}
127-
153+
const pluginTitle = getPluginNameFromSlug(audit.plugin, plugins);
128154
const auditTitle = link(
129-
`#${slugify(pluginAudit.title)}-${slugify(plugin.title)}`,
130-
pluginAudit?.title,
155+
`#${slugify(audit.title)}-${slugify(pluginTitle)}`,
156+
audit?.title,
131157
);
132-
133-
return (
134-
li(
135-
`${getSquaredScoreMarker(pluginAudit.score)} ${auditTitle} (_${
136-
plugin.title
137-
}_) - ${getAuditResult(pluginAudit)}`,
138-
) + NEW_LINE
158+
return li(
159+
`${getSquaredScoreMarker(
160+
audit.score,
161+
)} ${auditTitle} (_${pluginTitle}_) - ${getAuditResult(audit)}`,
139162
);
140163
}
141164

142-
function groupRefItemToCategorySection(
143-
refSlug: string,
144-
refPlugin: string,
165+
function groupItemToCategorySection(
166+
group: EnrichedScoredAuditGroupWithAudits,
145167
plugins: ScoredReport['plugins'],
146168
): string {
147-
const plugin = plugins.find(({ slug }) => slug === refPlugin) as PluginReport;
148-
const group = plugin?.groups?.find(
149-
({ slug }) => slug === refSlug,
150-
) as EnrichedScoredAuditGroup;
169+
const pluginTitle = getPluginNameFromSlug(group.plugin, plugins);
151170
const groupScore = Number(formatReportScore(group?.score || 0));
152-
153-
if (!group) {
154-
throwIsNotPresentError(`Group ${refSlug}`, plugin?.slug);
155-
}
156-
157171
const groupTitle = li(
158-
`${getRoundScoreMarker(groupScore)} ${group.title} (_${plugin.title}_)`,
172+
`${getRoundScoreMarker(groupScore)} ${group.title} (_${pluginTitle}_)`,
159173
);
160-
const foundAudits = group.refs.reduce<AuditReport[]>((acc, ref) => {
161-
const audit = plugin?.audits.find(
162-
({ slug: auditSlugInPluginAudits }) =>
163-
auditSlugInPluginAudits === ref.slug,
164-
);
165-
if (audit) {
166-
return [...acc, audit];
167-
}
168-
169-
return acc;
170-
}, []);
171-
172-
const groupAudits = foundAudits.reduce((acc, audit) => {
174+
const groupAudits = group.audits.reduce((acc, audit) => {
173175
const auditTitle = link(
174-
`#${slugify(audit.title)}-${slugify(plugin.title)}`,
176+
`#${slugify(audit.title)}-${slugify(pluginTitle)}`,
175177
audit?.title,
176178
);
177179
acc += ` ${li(
@@ -187,9 +189,12 @@ function groupRefItemToCategorySection(
187189
}
188190

189191
function reportToAuditsSection(report: ScoredReport): string {
190-
const auditsData = report.plugins.reduce((acc, plugin) => {
191-
const audits = plugin.audits.reduce((acc, audit) => {
192-
const auditTitle = `${audit.title} (${plugin.title})`;
192+
const auditsSection = report.plugins.reduce((acc, plugin) => {
193+
const auditsData = plugin.audits.sort(sortAudits).reduce((acc, audit) => {
194+
const auditTitle = `${audit.title} (${getPluginNameFromSlug(
195+
audit.plugin,
196+
report.plugins,
197+
)})`;
193198
const detailsTitle = `${getSquaredScoreMarker(
194199
audit.score,
195200
)} ${getAuditResult(audit, true)} (score: ${formatReportScore(
@@ -243,11 +248,10 @@ function reportToAuditsSection(report: ScoredReport): string {
243248

244249
return acc;
245250
}, '');
246-
247-
return acc + audits;
251+
return acc + auditsData;
248252
}, '');
249253

250-
return h2('🛡️ Audits') + NEW_LINE + NEW_LINE + auditsData;
254+
return h2('🛡️ Audits') + NEW_LINE + NEW_LINE + auditsSection;
251255
}
252256

253257
function reportToAboutSection(
@@ -325,7 +329,3 @@ function getAuditResult(audit: AuditReport, isHtml = false): string {
325329
? `<b>${displayValue || value}</b>`
326330
: style(String(displayValue || value));
327331
}
328-
329-
function throwIsNotPresentError(itemName: string, presentPlace: string): never {
330-
throw new Error(`${itemName} is not present in ${presentPlace}`);
331-
}

packages/utils/src/lib/report-to-stdout.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
formatReportScore,
1010
reportHeadlineText,
1111
reportRawOverviewTableHeaders,
12+
sortAudits,
1213
} from './report';
1314
import { ScoredReport } from './scoring';
1415

@@ -73,7 +74,7 @@ function reportToDetailSection(report: ScoredReport): string {
7374

7475
const ui = cliui({ width: 80 });
7576

76-
audits.forEach(({ score, title, displayValue, value }) => {
77+
audits.sort(sortAudits).forEach(({ score, title, displayValue, value }) => {
7778
ui.div(
7879
{
7980
text: withColor({ score, text: '●' }),

0 commit comments

Comments
 (0)