Skip to content

Commit 3225e8c

Browse files
committed
Move formatting function to new file, pass error to shortSummary
1 parent 411f134 commit 3225e8c

File tree

4 files changed

+322
-81
lines changed

4 files changed

+322
-81
lines changed

src/format.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const chalk = require('chalk');
2+
const { minify } = require('html-minifier');
3+
const { makeReplacements } = require('./replacements');
4+
5+
const belowThreshold = (id, expected, categories) => {
6+
const category = categories.find((c) => c.id === id);
7+
if (!category) {
8+
console.warn(`Could not find category ${chalk.yellow(id)}`);
9+
}
10+
const actual = category ? category.score : Number.MAX_SAFE_INTEGER;
11+
return actual < expected;
12+
};
13+
14+
const getError = (id, expected, categories, audits) => {
15+
const category = categories.find((c) => c.id === id);
16+
17+
const categoryError = `Expected category ${chalk.cyan(
18+
category.title,
19+
)} to be greater or equal to ${chalk.green(expected)} but got ${chalk.red(
20+
category.score !== null ? category.score : 'unknown',
21+
)}`;
22+
23+
const categoryAudits = category.auditRefs
24+
.filter(({ weight, id }) => weight > 0 && audits[id].score < 1)
25+
.map((ref) => {
26+
const audit = audits[ref.id];
27+
return ` '${chalk.cyan(
28+
audit.title,
29+
)}' received a score of ${chalk.yellow(audit.score)}`;
30+
})
31+
.join('\n');
32+
33+
return { message: categoryError, details: categoryAudits };
34+
};
35+
36+
const formatShortSummary = ({ categories, runtimeError }) => {
37+
if (runtimeError) {
38+
return runtimeError.message;
39+
}
40+
return categories
41+
.map(({ title, score }) => `${title}: ${Math.round(score * 100)}`)
42+
.join(', ');
43+
};
44+
45+
const formatResults = ({ results, thresholds }) => {
46+
const hasScores = !results.lhr.runtimeError;
47+
48+
const categories = Object.values(results.lhr.categories).map(
49+
({ title, score, id, auditRefs }) => ({ title, score, id, auditRefs }),
50+
);
51+
52+
const categoriesBelowThreshold =
53+
hasScores &&
54+
Object.entries(thresholds).filter(([id, expected]) =>
55+
belowThreshold(id, expected, categories),
56+
);
57+
58+
const errors =
59+
hasScores &&
60+
categoriesBelowThreshold.map(([id, expected]) =>
61+
getError(id, expected, categories, results.lhr.audits),
62+
);
63+
64+
const summary =
65+
hasScores &&
66+
categories.map(({ title, score, id }) => ({
67+
title,
68+
score,
69+
id,
70+
...(thresholds[id] ? { threshold: thresholds[id] } : {}),
71+
}));
72+
73+
const shortSummary = formatShortSummary({
74+
categories,
75+
runtimeError: results.lhr.runtimeError,
76+
});
77+
78+
const formattedReport = makeReplacements(results.report);
79+
80+
// Pull some additional details to pass to App
81+
const { formFactor, locale } = results.lhr.configSettings;
82+
const installable = results.lhr.audits['installable-manifest']?.score === 1;
83+
const details = { installable, formFactor, locale };
84+
85+
const report = minify(formattedReport, {
86+
removeAttributeQuotes: true,
87+
collapseWhitespace: true,
88+
removeRedundantAttributes: true,
89+
removeOptionalTags: true,
90+
removeEmptyElements: true,
91+
minifyCSS: true,
92+
minifyJS: true,
93+
});
94+
95+
return { summary, shortSummary, details, report, errors };
96+
};
97+
98+
module.exports = {
99+
belowThreshold,
100+
getError,
101+
formatShortSummary,
102+
formatResults,
103+
};

src/format.test.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
const {
2+
belowThreshold,
3+
getError,
4+
formatShortSummary,
5+
formatResults,
6+
} = require('./format');
7+
8+
describe('format', () => {
9+
const getCategories = ({ score }) => [
10+
{
11+
title: 'Performance',
12+
score,
13+
id: 'performance',
14+
auditRefs: [
15+
{ weight: 1, id: 'is-crawlable' },
16+
{ weight: 1, id: 'robots-txt' },
17+
{ weight: 1, id: 'tap-targets' },
18+
],
19+
},
20+
];
21+
const audits = {
22+
'is-crawlable': {
23+
id: 'is-crawlable',
24+
title: 'Page isn’t blocked from indexing',
25+
description:
26+
"Search engines are unable to include your pages in search results if they don't have permission to crawl them. [Learn more](https://web.dev/is-crawable/).",
27+
score: 1,
28+
},
29+
'robots-txt': {
30+
id: 'robots-txt',
31+
title: 'robots.txt is valid',
32+
description:
33+
'If your robots.txt file is malformed, crawlers may not be able to understand how you want your website to be crawled or indexed. [Learn more](https://web.dev/robots-txt/).',
34+
score: 0,
35+
},
36+
'tap-targets': {
37+
id: 'tap-targets',
38+
title: 'Tap targets are sized appropriately',
39+
description:
40+
'Interactive elements like buttons and links should be large enough (48x48px), and have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more](https://web.dev/tap-targets/).',
41+
score: 0.5,
42+
},
43+
};
44+
45+
// Awkward formatting as the strings contain ANSI escape codes for colours.
46+
const formattedError = {
47+
details:
48+
" '\x1B[36mrobots.txt is valid\x1B[39m' received a score of \x1B[33m0\x1B[39m\n" +
49+
" '\x1B[36mTap targets are sized appropriately\x1B[39m' received a score of \x1B[33m0.5\x1B[39m",
50+
message:
51+
'Expected category \x1B[36mPerformance\x1B[39m to be greater or equal to \x1B[32m1\x1B[39m but got \x1B[31m0.5\x1B[39m',
52+
};
53+
54+
it('returns an expected error message and list of details with valid score', () => {
55+
const errorMessage = getError(
56+
'performance',
57+
1,
58+
getCategories({ score: 0.5 }),
59+
audits,
60+
);
61+
expect(errorMessage).toEqual(formattedError);
62+
});
63+
64+
describe('belowThreshold', () => {
65+
const categories = [
66+
{ title: 'Performance', score: 0.9, id: 'performance' },
67+
{ title: 'Accessibility', score: 0.8, id: 'accessibility' },
68+
];
69+
70+
it('returns false when the score is above the threshold', () => {
71+
expect(belowThreshold('performance', 0.8, categories)).toBe(false);
72+
});
73+
74+
it('returns false when the category is not found', () => {
75+
console.warn = jest.fn();
76+
const result = belowThreshold('seo', 0.8, categories);
77+
expect(console.warn).toHaveBeenCalled();
78+
expect(result).toBe(false);
79+
});
80+
81+
it('returns true when the score is below the threshold', () => {
82+
expect(belowThreshold('performance', 1, categories)).toBe(true);
83+
});
84+
});
85+
86+
describe('getError', () => {
87+
it('returns an expected error message and list of details without valid score', () => {
88+
const errorMessage = getError(
89+
'performance',
90+
1,
91+
getCategories({ score: null }),
92+
audits,
93+
);
94+
// Matching is awkward as the strings contain ANSI escape codes for colours.
95+
expect(errorMessage.message).toContain(
96+
'to be greater or equal to \x1B[32m1\x1B[39m but got \x1B[31munknown\x1B[39m',
97+
);
98+
});
99+
});
100+
101+
describe('formatShortSummary', () => {
102+
const categories = [
103+
{ title: 'Performance', score: 1, id: 'performance' },
104+
{ title: 'Accessibility', score: 0.9, id: 'accessibility' },
105+
{ title: 'Best Practices', score: 0.8, id: 'best-practices' },
106+
{ title: 'SEO', score: 0.7, id: 'seo' },
107+
{ title: 'PWA', score: 0.6, id: 'pwa' },
108+
];
109+
const runtimeError = {
110+
code: 'NO_FCP',
111+
message:
112+
'The page did not paint any content. Please ensure you keep the browser window in the foreground during the load and try again. (NO_FCP)',
113+
};
114+
115+
it('should return a shortSummary containing scores if available', () => {
116+
const shortSummary = formatShortSummary({ categories });
117+
expect(shortSummary).toEqual(
118+
'Performance: 100, Accessibility: 90, Best Practices: 80, SEO: 70, PWA: 60',
119+
);
120+
});
121+
122+
it('should return a shortSummary error message', () => {
123+
const shortSummary = formatShortSummary({
124+
runtimeError,
125+
});
126+
expect(shortSummary).toEqual(runtimeError.message);
127+
});
128+
});
129+
130+
describe('formatResults', () => {
131+
const getResults = () => ({
132+
lhr: {
133+
lighthouseVersion: '9.6.3',
134+
requestedUrl: 'http://localhost:5100/404.html',
135+
finalUrl: 'http://localhost:5100/404.html',
136+
audits,
137+
configSettings: {},
138+
categories: getCategories({ score: 0.5 }),
139+
},
140+
artifacts: {},
141+
report: '<!doctype html>\n' + '<html lang="en">Hi</html>\n',
142+
});
143+
144+
it('should return formatted results', () => {
145+
expect(formatResults({ results: getResults(), thresholds: {} })).toEqual({
146+
details: {
147+
formFactor: undefined,
148+
installable: false,
149+
locale: undefined,
150+
},
151+
errors: [],
152+
report: '<!doctype html><html lang=en>Hi',
153+
shortSummary: 'Performance: 50',
154+
summary: [{ id: 'performance', score: 0.5, title: 'Performance' }],
155+
});
156+
});
157+
158+
it('should return formatted results with passing thresholds', () => {
159+
const thresholds = {
160+
performance: 0.1,
161+
};
162+
const formattedResults = formatResults({
163+
results: getResults(),
164+
thresholds,
165+
});
166+
expect(formattedResults.errors).toEqual([]);
167+
expect(formattedResults.summary).toEqual([
168+
{
169+
id: 'performance',
170+
score: 0.5,
171+
title: 'Performance',
172+
threshold: 0.1,
173+
},
174+
]);
175+
});
176+
177+
it('should return formatted results with failing thresholds', () => {
178+
const thresholds = {
179+
performance: 1,
180+
};
181+
const formattedResults = formatResults({
182+
results: getResults(),
183+
thresholds,
184+
});
185+
expect(formattedResults.errors).toEqual([formattedError]);
186+
expect(formattedResults.summary).toEqual([
187+
{
188+
id: 'performance',
189+
score: 0.5,
190+
title: 'Performance',
191+
threshold: 1,
192+
},
193+
]);
194+
});
195+
196+
it('should use supplied config settings and data to populate `details`', () => {
197+
const results = getResults();
198+
results.lhr.configSettings = {
199+
locale: 'es',
200+
formFactor: 'desktop',
201+
};
202+
results.lhr.audits['installable-manifest'] = {
203+
id: 'installable-manifest',
204+
score: 1,
205+
};
206+
207+
const formattedResults = formatResults({ results, thresholds: {} });
208+
expect(formattedResults.details).toEqual({
209+
formFactor: 'desktop',
210+
installable: true,
211+
locale: 'es',
212+
});
213+
});
214+
});
215+
});

0 commit comments

Comments
 (0)