From 02298e9d8ad12473f269553b4594d1f33aa24e60 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 10 Oct 2024 18:57:41 -0400 Subject: [PATCH 1/2] feat: skip perfect scores in stdout summary --- packages/core/src/lib/collect-and-persist.ts | 18 ++- .../src/lib/collect-and-persist.unit.test.ts | 39 +++++++ .../core/src/lib/implementation/persist.ts | 9 +- .../lib/implementation/persist.unit.test.ts | 31 ++--- .../report-stdout-all-perfect-scores.txt | 31 +++++ .../report-stdout-no-categories.txt | 57 +--------- .../__snapshots__/report-stdout-verbose.txt | 100 +++++++++++++++++ .../reports/__snapshots__/report-stdout.txt | 57 +--------- .../log-stdout-summary.integration.test.ts | 33 ++++++ .../src/lib/reports/log-stdout-summary.ts | 75 ++++++++----- .../reports/log-stdout-summary.unit.test.ts | 106 +++++++++++++++++- 11 files changed, 386 insertions(+), 170 deletions(-) create mode 100644 packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt create mode 100644 packages/utils/src/lib/reports/__snapshots__/report-stdout-verbose.txt diff --git a/packages/core/src/lib/collect-and-persist.ts b/packages/core/src/lib/collect-and-persist.ts index 0d992cb96..b1ff94648 100644 --- a/packages/core/src/lib/collect-and-persist.ts +++ b/packages/core/src/lib/collect-and-persist.ts @@ -3,7 +3,12 @@ import { type PersistConfig, pluginReportSchema, } from '@code-pushup/models'; -import { verboseUtils } from '@code-pushup/utils'; +import { + logStdoutSummary, + scoreReport, + sortReport, + verboseUtils, +} from '@code-pushup/utils'; import { collect } from './implementation/collect'; import { logPersistedResults, persistReport } from './implementation/persist'; import type { GlobalOptions } from './types'; @@ -18,7 +23,16 @@ export async function collectAndPersistReports( const { exec } = verboseUtils(options.verbose); const report = await collect(options); - const persistResults = await persistReport(report, options.persist); + const sortedScoredReport = sortReport(scoreReport(report)); + const persistResults = await persistReport( + report, + sortedScoredReport, + options.persist, + ); + + // terminal output + logStdoutSummary(sortedScoredReport, options.verbose); + exec(() => { logPersistedResults(persistResults); }); diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index 30d7615f1..1e7a5188d 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -3,7 +3,15 @@ import { ISO_STRING_REGEXP, MINIMAL_CONFIG_MOCK, MINIMAL_REPORT_MOCK, + getLogMessages, } from '@code-pushup/test-utils'; +import { + logStdoutSummary, + scoreReport, + sortReport, + ui, +} from '@code-pushup/utils'; +import * as utils from '@code-pushup/utils'; import { type CollectAndPersistReportsOptions, collectAndPersistReports, @@ -21,7 +29,16 @@ vi.mock('./implementation/persist', () => ({ })); describe('collectAndPersistReports', () => { + beforeEach(() => { + vi.spyOn(utils, 'logStdoutSummary'); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + it('should call collect and persistReport with correct parameters in non-verbose mode', async () => { + const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); const nonVerboseConfig: CollectAndPersistReportsOptions = { categories: [], ...MINIMAL_CONFIG_MOCK, @@ -49,6 +66,7 @@ describe('collectAndPersistReports', () => { categories: expect.any(Array), plugins: expect.any(Array), }, + sortedScoredReport, { outputDir: 'output', filename: 'report', @@ -56,10 +74,12 @@ describe('collectAndPersistReports', () => { }, ); + expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport, false); expect(logPersistedResults).not.toHaveBeenCalled(); }); it('should call collect and persistReport with correct parameters in verbose mode', async () => { + const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); const verboseConfig: CollectAndPersistReportsOptions = { categories: [], ...MINIMAL_CONFIG_MOCK, @@ -77,9 +97,28 @@ describe('collectAndPersistReports', () => { expect(persistReport).toHaveBeenCalledWith( MINIMAL_REPORT_MOCK, + sortedScoredReport, verboseConfig.persist, ); + expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport, true); expect(logPersistedResults).toHaveBeenCalled(); }); + + it('should print a summary to stdout`', async () => { + const verboseConfig: CollectAndPersistReportsOptions = { + categories: [], + ...MINIMAL_CONFIG_MOCK, + persist: { + outputDir: 'output', + filename: 'report', + format: ['md'], + }, + verbose: true, + progress: false, + }; + await collectAndPersistReports(verboseConfig); + const logs = getLogMessages(ui().logger); + expect(logs.at(-2)).toContain('Made with ❤ by code-pushup.dev'); + }); }); diff --git a/packages/core/src/lib/implementation/persist.ts b/packages/core/src/lib/implementation/persist.ts index 9ea81f392..4a1a5ebdd 100644 --- a/packages/core/src/lib/implementation/persist.ts +++ b/packages/core/src/lib/implementation/persist.ts @@ -3,12 +3,10 @@ import { join } from 'node:path'; import type { PersistConfig, Report } from '@code-pushup/models'; import { type MultipleFileResults, + type ScoredReport, directoryExists, generateMdReport, logMultipleFileResults, - logStdoutSummary, - scoreReport, - sortReport, ui, } from '@code-pushup/utils'; @@ -26,14 +24,11 @@ export class PersistError extends Error { export async function persistReport( report: Report, + sortedScoredReport: ScoredReport, options: Required, ): Promise { const { outputDir, filename, format } = options; - const sortedScoredReport = sortReport(scoreReport(report)); - // terminal output - logStdoutSummary(sortedScoredReport); - // collect physical format outputs const results = format.map(reportType => { switch (reportType) { diff --git a/packages/core/src/lib/implementation/persist.unit.test.ts b/packages/core/src/lib/implementation/persist.unit.test.ts index d377484cb..74f9c6b9f 100644 --- a/packages/core/src/lib/implementation/persist.unit.test.ts +++ b/packages/core/src/lib/implementation/persist.unit.test.ts @@ -9,7 +9,7 @@ import { REPORT_MOCK, getLogMessages, } from '@code-pushup/test-utils'; -import { ui } from '@code-pushup/utils'; +import { scoreReport, sortReport, ui } from '@code-pushup/utils'; import { logPersistedResults, persistReport } from './persist'; describe('persistReport', () => { @@ -17,28 +17,9 @@ describe('persistReport', () => { vol.fromJSON({}, MEMFS_VOLUME); }); - it('should print a summary to stdout when no format is specified`', async () => { - await persistReport(MINIMAL_REPORT_MOCK, { - outputDir: MEMFS_VOLUME, - filename: 'report', - format: [], - }); - const logs = getLogMessages(ui().logger); - expect(logs.at(-2)).toContain('Made with ❤ by code-pushup.dev'); - }); - - it('should print a summary to stdout when all formats are specified`', async () => { - await persistReport(MINIMAL_REPORT_MOCK, { - outputDir: MEMFS_VOLUME, - filename: 'report', - format: ['md', 'json'], - }); - const logs = getLogMessages(ui().logger); - expect(logs.at(-2)).toContain('Made with ❤ by code-pushup.dev'); - }); - it('should create a report in json format', async () => { - await persistReport(MINIMAL_REPORT_MOCK, { + const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); + await persistReport(MINIMAL_REPORT_MOCK, sortedScoredReport, { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json'], @@ -60,7 +41,8 @@ describe('persistReport', () => { }); it('should create a report in md format', async () => { - await persistReport(MINIMAL_REPORT_MOCK, { + const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); + await persistReport(MINIMAL_REPORT_MOCK, sortedScoredReport, { outputDir: MEMFS_VOLUME, filename: 'report', format: ['md'], @@ -75,7 +57,8 @@ describe('persistReport', () => { }); it('should create a report with categories section in all formats', async () => { - await persistReport(REPORT_MOCK, { + const sortedScoredReport = sortReport(scoreReport(REPORT_MOCK)); + await persistReport(REPORT_MOCK, sortedScoredReport, { outputDir: MEMFS_VOLUME, format: ['md', 'json'], filename: 'report', diff --git a/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt b/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt new file mode 100644 index 000000000..51e3e48d5 --- /dev/null +++ b/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt @@ -0,0 +1,31 @@ +Code PushUp Report - @code-pushup/core@0.0.1 + + +ESLint audits + +● All audits have perfect scores + + +Lighthouse audits + +● Minimize third-party usage Third-party code + blocked the main + thread for 6,850 ms +● First Contentful Paint 1.2 s +● Largest Contentful Paint 1.5 s +● Speed Index 1.2 s +● ... 2 audits with perfect scores omitted for brevity ... + +Categories + +┌─────────────────────────────────────────────────────────┬─────────┬──────────┐ +│ Category │ Score │ Audits │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Performance │ 92 │ 8 │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Bug prevention │ 100 │ 16 │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Code style │ 100 │ 13 │ +└─────────────────────────────────────────────────────────┴─────────┴──────────┘ + +Made with ❤ by code-pushup.dev diff --git a/packages/utils/src/lib/reports/__snapshots__/report-stdout-no-categories.txt b/packages/utils/src/lib/reports/__snapshots__/report-stdout-no-categories.txt index bec627211..a2cb18f1f 100644 --- a/packages/utils/src/lib/reports/__snapshots__/report-stdout-no-categories.txt +++ b/packages/utils/src/lib/reports/__snapshots__/report-stdout-no-categories.txt @@ -19,59 +19,7 @@ ESLint audits never reassigned after declared ● Require braces around arrow function bodies 1 warning ● Require the use of `===` and `!==` 1 warning -● Disallow `target="_blank"` attribute without passed - `rel="noreferrer"` -● Disallow assignment operators in conditional passed - expressions -● Disallow comments from being inserted as text nodes passed -● Disallow direct mutation of this.state passed -● Disallow duplicate properties in JSX passed -● Disallow invalid regular expression strings in `RegExp` passed - constructors -● Disallow loops with a body that allows only one passed - iteration -● Disallow missing displayName in a React component passed - definition -● Disallow missing React when using JSX passed -● Disallow negating the left operand of relational passed - operators -● Disallow passing of children as props passed -● Disallow React to be incorrectly marked as unused passed -● Disallow reassigning `const` variables passed -● Disallow the use of `debugger` passed -● Disallow the use of undeclared variables unless passed - mentioned in `/*global */` comments -● Disallow undeclared variables in JSX passed -● Disallow unescaped HTML entities from appearing in passed - markup -● Disallow usage of deprecated methods passed -● Disallow usage of findDOMNode passed -● Disallow usage of isMounted passed -● Disallow usage of the return value of ReactDOM.render passed -● Disallow usage of unknown DOM property passed -● Disallow use of optional chaining in contexts where the passed - `undefined` value is not allowed -● Disallow using Object.assign with an object literal as passed - the first argument and prefer the use of object spread - instead -● Disallow using string references passed -● Disallow variables used in JSX to be incorrectly marked passed - as unused -● Disallow when a DOM element is using both children and passed - dangerouslySetInnerHTML -● Enforce a maximum number of lines per file passed -● Enforce camelcase naming convention passed -● Enforce comparing `typeof` expressions against valid passed - strings -● Enforce consistent brace style for all control passed - statements -● Enforce ES5 or ES6 class for returning value in render passed - function -● enforces the Rules of Hooks passed -● Require `let` or `const` instead of `var` passed -● Require calls to `isNaN()` when checking for `NaN` passed -● Require or disallow "Yoda" conditions passed -● Require using arrow functions for callbacks passed +● ... 37 audits with perfect scores omitted for brevity ... Lighthouse audits @@ -82,7 +30,6 @@ Lighthouse audits ● First Contentful Paint 1.2 s ● Largest Contentful Paint 1.5 s ● Speed Index 1.2 s -● Cumulative Layout Shift 0 -● Total Blocking Time 0 ms +● ... 2 audits with perfect scores omitted for brevity ... Made with ❤ by code-pushup.dev diff --git a/packages/utils/src/lib/reports/__snapshots__/report-stdout-verbose.txt b/packages/utils/src/lib/reports/__snapshots__/report-stdout-verbose.txt new file mode 100644 index 000000000..0a5bb1a56 --- /dev/null +++ b/packages/utils/src/lib/reports/__snapshots__/report-stdout-verbose.txt @@ -0,0 +1,100 @@ +Code PushUp Report - @code-pushup/core@0.0.1 + + +ESLint audits + +● Disallow missing props validation in a React component 6 warnings + definition +● Disallow variable declarations from shadowing variables 3 warnings + declared in the outer scope +● Require or disallow method and property shorthand 3 warnings + syntax for object literals +● verifies the list of dependencies for Hooks like 2 warnings + useEffect and similar +● Disallow missing `key` props in iterators/collection 1 warning + literals +● Disallow unused variables 1 warning +● Enforce a maximum number of lines of code in a function 1 warning +● Require `const` declarations for variables that are 1 warning + never reassigned after declared +● Require braces around arrow function bodies 1 warning +● Require the use of `===` and `!==` 1 warning +● Disallow `target="_blank"` attribute without passed + `rel="noreferrer"` +● Disallow assignment operators in conditional passed + expressions +● Disallow comments from being inserted as text nodes passed +● Disallow direct mutation of this.state passed +● Disallow duplicate properties in JSX passed +● Disallow invalid regular expression strings in `RegExp` passed + constructors +● Disallow loops with a body that allows only one passed + iteration +● Disallow missing displayName in a React component passed + definition +● Disallow missing React when using JSX passed +● Disallow negating the left operand of relational passed + operators +● Disallow passing of children as props passed +● Disallow React to be incorrectly marked as unused passed +● Disallow reassigning `const` variables passed +● Disallow the use of `debugger` passed +● Disallow the use of undeclared variables unless passed + mentioned in `/*global */` comments +● Disallow undeclared variables in JSX passed +● Disallow unescaped HTML entities from appearing in passed + markup +● Disallow usage of deprecated methods passed +● Disallow usage of findDOMNode passed +● Disallow usage of isMounted passed +● Disallow usage of the return value of ReactDOM.render passed +● Disallow usage of unknown DOM property passed +● Disallow use of optional chaining in contexts where the passed + `undefined` value is not allowed +● Disallow using Object.assign with an object literal as passed + the first argument and prefer the use of object spread + instead +● Disallow using string references passed +● Disallow variables used in JSX to be incorrectly marked passed + as unused +● Disallow when a DOM element is using both children and passed + dangerouslySetInnerHTML +● Enforce a maximum number of lines per file passed +● Enforce camelcase naming convention passed +● Enforce comparing `typeof` expressions against valid passed + strings +● Enforce consistent brace style for all control passed + statements +● Enforce ES5 or ES6 class for returning value in render passed + function +● enforces the Rules of Hooks passed +● Require `let` or `const` instead of `var` passed +● Require calls to `isNaN()` when checking for `NaN` passed +● Require or disallow "Yoda" conditions passed +● Require using arrow functions for callbacks passed + + +Lighthouse audits + +● Minimize third-party usage Third-party code + blocked the main + thread for 6,850 ms +● First Contentful Paint 1.2 s +● Largest Contentful Paint 1.5 s +● Speed Index 1.2 s +● Cumulative Layout Shift 0 +● Total Blocking Time 0 ms + +Categories + +┌─────────────────────────────────────────────────────────┬─────────┬──────────┐ +│ Category │ Score │ Audits │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Performance │ 92 │ 8 │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Bug prevention │ 68 │ 16 │ +├─────────────────────────────────────────────────────────┼─────────┼──────────┤ +│ Code style │ 54 │ 13 │ +└─────────────────────────────────────────────────────────┴─────────┴──────────┘ + +Made with ❤ by code-pushup.dev diff --git a/packages/utils/src/lib/reports/__snapshots__/report-stdout.txt b/packages/utils/src/lib/reports/__snapshots__/report-stdout.txt index 0a5bb1a56..c7ebf3e24 100644 --- a/packages/utils/src/lib/reports/__snapshots__/report-stdout.txt +++ b/packages/utils/src/lib/reports/__snapshots__/report-stdout.txt @@ -19,59 +19,7 @@ ESLint audits never reassigned after declared ● Require braces around arrow function bodies 1 warning ● Require the use of `===` and `!==` 1 warning -● Disallow `target="_blank"` attribute without passed - `rel="noreferrer"` -● Disallow assignment operators in conditional passed - expressions -● Disallow comments from being inserted as text nodes passed -● Disallow direct mutation of this.state passed -● Disallow duplicate properties in JSX passed -● Disallow invalid regular expression strings in `RegExp` passed - constructors -● Disallow loops with a body that allows only one passed - iteration -● Disallow missing displayName in a React component passed - definition -● Disallow missing React when using JSX passed -● Disallow negating the left operand of relational passed - operators -● Disallow passing of children as props passed -● Disallow React to be incorrectly marked as unused passed -● Disallow reassigning `const` variables passed -● Disallow the use of `debugger` passed -● Disallow the use of undeclared variables unless passed - mentioned in `/*global */` comments -● Disallow undeclared variables in JSX passed -● Disallow unescaped HTML entities from appearing in passed - markup -● Disallow usage of deprecated methods passed -● Disallow usage of findDOMNode passed -● Disallow usage of isMounted passed -● Disallow usage of the return value of ReactDOM.render passed -● Disallow usage of unknown DOM property passed -● Disallow use of optional chaining in contexts where the passed - `undefined` value is not allowed -● Disallow using Object.assign with an object literal as passed - the first argument and prefer the use of object spread - instead -● Disallow using string references passed -● Disallow variables used in JSX to be incorrectly marked passed - as unused -● Disallow when a DOM element is using both children and passed - dangerouslySetInnerHTML -● Enforce a maximum number of lines per file passed -● Enforce camelcase naming convention passed -● Enforce comparing `typeof` expressions against valid passed - strings -● Enforce consistent brace style for all control passed - statements -● Enforce ES5 or ES6 class for returning value in render passed - function -● enforces the Rules of Hooks passed -● Require `let` or `const` instead of `var` passed -● Require calls to `isNaN()` when checking for `NaN` passed -● Require or disallow "Yoda" conditions passed -● Require using arrow functions for callbacks passed +● ... 37 audits with perfect scores omitted for brevity ... Lighthouse audits @@ -82,8 +30,7 @@ Lighthouse audits ● First Contentful Paint 1.2 s ● Largest Contentful Paint 1.5 s ● Speed Index 1.2 s -● Cumulative Layout Shift 0 -● Total Blocking Time 0 ms +● ... 2 audits with perfect scores omitted for brevity ... Categories diff --git a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts index 934747604..5a63d8e80 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts @@ -48,4 +48,37 @@ describe('logStdoutSummary', () => { '__snapshots__/report-stdout-no-categories.txt', ); }); + + it('should include all audits when verbose is true', async () => { + logStdoutSummary(sortReport(scoreReport(reportMock())), true); + + const output = logs.join('\n'); + + await expect(removeColorCodes(output)).toMatchFileSnapshot( + '__snapshots__/report-stdout-verbose.txt', + ); + }); + + it('should indicate that all audits have perfect scores', async () => { + const report = reportMock(); + const reportWithPerfectScores = { + ...report, + plugins: report.plugins.map((plugin, index) => ({ + ...plugin, + audits: plugin.audits.map(audit => ({ + ...audit, + score: index === 0 ? 1 : audit.score, + })), + })), + }; + + logStdoutSummary(sortReport(scoreReport(reportWithPerfectScores))); + + const output = logs.join('\n'); + + expect(output).toContain('All audits have perfect scores'); + await expect(removeColorCodes(output)).toMatchFileSnapshot( + '__snapshots__/report-stdout-all-perfect-scores.txt', + ); + }); }); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.ts b/packages/utils/src/lib/reports/log-stdout-summary.ts index c9a9b5a48..3dc4f7fdf 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.ts @@ -15,12 +15,12 @@ function log(msg = ''): void { ui().logger.log(msg); } -export function logStdoutSummary(report: ScoredReport): void { +export function logStdoutSummary(report: ScoredReport, verbose = false): void { const printCategories = report.categories.length > 0; log(reportToHeaderSection(report)); log(); - logPlugins(report); + logPlugins(report.plugins, verbose); if (printCategories) { logCategories(report); } @@ -33,38 +33,61 @@ function reportToHeaderSection(report: ScoredReport): string { return `${bold(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`; } -function logPlugins(report: ScoredReport): void { - const { plugins } = report; - +export function logPlugins( + plugins: ScoredReport['plugins'], + verbose: boolean, +): void { plugins.forEach(plugin => { const { title, audits } = plugin; - log(); - log(bold.magentaBright(`${title} audits`)); - log(); - audits.forEach((audit: AuditReport) => { + const filteredAudits = verbose + ? audits + : audits.filter(({ score }) => score !== 1); + const diff = audits.length - filteredAudits.length; + + logAuditRows(title, filteredAudits); + + if (diff > 0) { + const message = + filteredAudits.length === 0 + ? 'All audits have perfect scores' + : `... ${diff} audits with perfect scores omitted for brevity ...`; + ui().row([ - { - text: applyScoreColor({ score: audit.score, text: '●' }), - width: 2, - padding: [0, 1, 0, 0], - }, - { - text: audit.title, - // eslint-disable-next-line no-magic-numbers - padding: [0, 3, 0, 0], - }, - { - text: cyanBright(audit.displayValue || `${audit.value}`), - // eslint-disable-next-line no-magic-numbers - width: 20, - padding: [0, 0, 0, 0], - }, + { text: '●', width: 2, padding: [0, 1, 0, 0] }, + // eslint-disable-next-line no-magic-numbers + { text: message, padding: [0, 3, 0, 0] }, ]); - }); + } log(); }); } +function logAuditRows(title: string, audits: AuditReport[]): void { + log(); + log(bold.magentaBright(`${title} audits`)); + log(); + audits.forEach((audit: AuditReport) => { + ui().row([ + { + text: applyScoreColor({ score: audit.score, text: '●' }), + width: 2, + padding: [0, 1, 0, 0], + }, + { + text: audit.title, + // eslint-disable-next-line no-magic-numbers + padding: [0, 3, 0, 0], + }, + { + text: cyanBright(audit.displayValue || `${audit.value}`), + // eslint-disable-next-line no-magic-numbers + width: 20, + padding: [0, 0, 0, 0], + }, + ]); + }); +} + export function logCategories({ categories, plugins }: ScoredReport): void { const hAlign = (idx: number) => (idx === 0 ? 'left' : 'right'); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts index 51eadf54d..2884b17ba 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts @@ -1,7 +1,11 @@ import { beforeAll, describe, expect, vi } from 'vitest'; import { removeColorCodes } from '@code-pushup/test-utils'; import { ui } from '../logging'; -import { binaryIconPrefix, logCategories } from './log-stdout-summary'; +import { + binaryIconPrefix, + logCategories, + logPlugins, +} from './log-stdout-summary'; import type { ScoredReport } from './types'; describe('logCategories', () => { @@ -157,6 +161,102 @@ describe('logCategories', () => { }); }); +describe('logPlugins', () => { + let logs: string[]; + + beforeAll(() => { + logs = []; + vi.spyOn(console, 'log').mockImplementation(msg => { + logs = [...logs, msg]; + }); + ui().switchMode('normal'); + }); + + afterEach(() => { + logs = []; + }); + + afterAll(() => { + ui().switchMode('raw'); + }); + + it('should log only audits with scores other than 1 when verbose is false', () => { + logPlugins( + [ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.75, value: 75 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins'], + false, + ); + const output = logs.join('\n'); + expect(output).toContain('Audit 1'); + expect(output).not.toContain('Audit 2'); + expect(output).toContain('audits with perfect scores omitted for brevity'); + }); + + it('should log all audits when verbose is true', () => { + logPlugins( + [ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.5, value: 50 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins'], + true, + ); + const output = logs.join('\n'); + expect(output).toContain('Audit 1'); + expect(output).toContain('Audit 2'); + }); + + it('should indicate all audits have perfect scores', () => { + logPlugins( + [ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 1, value: 100 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins'], + false, + ); + const output = logs.join('\n'); + expect(output).toContain('All audits have perfect scores'); + }); + + it('should log original audits when verbose is false and no audits have perfect scores', () => { + logPlugins( + [ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.5, value: 100 }, + { title: 'Audit 2', score: 0.5, value: 100 }, + ], + }, + ] as ScoredReport['plugins'], + false, + ); + const output = logs.join('\n'); + expect(output).toContain('Audit 1'); + expect(output).toContain('Audit 2'); + }); +}); + describe('binaryIconPrefix', () => { it('should return passing binaryPrefix if score is 1 and isBinary is true', () => { expect(removeColorCodes(binaryIconPrefix(1, true))).toBe('✓ '); @@ -169,4 +269,8 @@ describe('binaryIconPrefix', () => { it('should return NO binaryPrefix if score is 1 and isBinary is false', () => { expect(binaryIconPrefix(1, false)).toBe(''); }); + + it('should return NO binaryPrefix when isBinary is undefined', () => { + expect(binaryIconPrefix(1, undefined)).toBe(''); + }); }); From b7382885d696257e02d5ffdd5c6fa7b4cc37ab25 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 11 Oct 2024 10:26:06 -0400 Subject: [PATCH 2/2] feat: skip perfect scores in stdout summary --- .../src/lib/collect-and-persist.unit.test.ts | 31 +++++---- .../report-stdout-all-perfect-scores.txt | 2 +- .../log-stdout-summary.integration.test.ts | 17 ++--- .../src/lib/reports/log-stdout-summary.ts | 63 ++++++++++--------- .../reports/log-stdout-summary.unit.test.ts | 2 +- 5 files changed, 58 insertions(+), 57 deletions(-) diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index 1e7a5188d..1bfa017b1 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -1,4 +1,4 @@ -import { describe } from 'vitest'; +import { type MockInstance, describe } from 'vitest'; import { ISO_STRING_REGEXP, MINIMAL_CONFIG_MOCK, @@ -6,6 +6,7 @@ import { getLogMessages, } from '@code-pushup/test-utils'; import { + type ScoredReport, logStdoutSummary, scoreReport, sortReport, @@ -29,12 +30,17 @@ vi.mock('./implementation/persist', () => ({ })); describe('collectAndPersistReports', () => { + let logStdoutSpy: MockInstance< + [report: ScoredReport, verbose?: boolean], + void + >; + beforeEach(() => { - vi.spyOn(utils, 'logStdoutSummary'); + logStdoutSpy = vi.spyOn(utils, 'logStdoutSummary'); }); - afterEach(() => { - vi.clearAllMocks(); + afterAll(() => { + logStdoutSpy.mockRestore(); }); it('should call collect and persistReport with correct parameters in non-verbose mode', async () => { @@ -105,19 +111,10 @@ describe('collectAndPersistReports', () => { expect(logPersistedResults).toHaveBeenCalled(); }); - it('should print a summary to stdout`', async () => { - const verboseConfig: CollectAndPersistReportsOptions = { - categories: [], - ...MINIMAL_CONFIG_MOCK, - persist: { - outputDir: 'output', - filename: 'report', - format: ['md'], - }, - verbose: true, - progress: false, - }; - await collectAndPersistReports(verboseConfig); + it('should print a summary to stdout', async () => { + await collectAndPersistReports( + MINIMAL_CONFIG_MOCK as CollectAndPersistReportsOptions, + ); const logs = getLogMessages(ui().logger); expect(logs.at(-2)).toContain('Made with ❤ by code-pushup.dev'); }); diff --git a/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt b/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt index 51e3e48d5..48b72e8ee 100644 --- a/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt +++ b/packages/utils/src/lib/reports/__snapshots__/report-stdout-all-perfect-scores.txt @@ -3,7 +3,7 @@ Code PushUp Report - @code-pushup/core@0.0.1 ESLint audits -● All audits have perfect scores +● ... All 47 audits have perfect scores ... Lighthouse audits diff --git a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts index 5a63d8e80..072e0b274 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts @@ -63,20 +63,21 @@ describe('logStdoutSummary', () => { const report = reportMock(); const reportWithPerfectScores = { ...report, - plugins: report.plugins.map((plugin, index) => ({ - ...plugin, - audits: plugin.audits.map(audit => ({ - ...audit, - score: index === 0 ? 1 : audit.score, - })), - })), + plugins: report.plugins.map((plugin, index) => + index > 0 + ? plugin + : { + ...plugin, + audits: plugin.audits.map(audit => ({ ...audit, score: 1 })), + }, + ), }; logStdoutSummary(sortReport(scoreReport(reportWithPerfectScores))); const output = logs.join('\n'); - expect(output).toContain('All audits have perfect scores'); + expect(output).toContain('All 47 audits have perfect scores'); await expect(removeColorCodes(output)).toMatchFileSnapshot( '__snapshots__/report-stdout-all-perfect-scores.txt', ); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.ts b/packages/utils/src/lib/reports/log-stdout-summary.ts index 3dc4f7fdf..d4918adde 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.ts @@ -44,50 +44,53 @@ export function logPlugins( : audits.filter(({ score }) => score !== 1); const diff = audits.length - filteredAudits.length; - logAuditRows(title, filteredAudits); + logAudits(title, filteredAudits); if (diff > 0) { - const message = + const notice = filteredAudits.length === 0 - ? 'All audits have perfect scores' + ? `... All ${diff} audits have perfect scores ...` : `... ${diff} audits with perfect scores omitted for brevity ...`; - - ui().row([ - { text: '●', width: 2, padding: [0, 1, 0, 0] }, - // eslint-disable-next-line no-magic-numbers - { text: message, padding: [0, 3, 0, 0] }, - ]); + logRow(1, notice); } log(); }); } -function logAuditRows(title: string, audits: AuditReport[]): void { +function logAudits(pluginTitle: string, audits: AuditReport[]): void { log(); - log(bold.magentaBright(`${title} audits`)); + log(bold.magentaBright(`${pluginTitle} audits`)); log(); - audits.forEach((audit: AuditReport) => { - ui().row([ - { - text: applyScoreColor({ score: audit.score, text: '●' }), - width: 2, - padding: [0, 1, 0, 0], - }, - { - text: audit.title, - // eslint-disable-next-line no-magic-numbers - padding: [0, 3, 0, 0], - }, - { - text: cyanBright(audit.displayValue || `${audit.value}`), - // eslint-disable-next-line no-magic-numbers - width: 20, - padding: [0, 0, 0, 0], - }, - ]); + audits.forEach(({ score, title, displayValue, value }) => { + logRow(score, title, displayValue || `${value}`); }); } +function logRow(score: number, title: string, value?: string): void { + ui().row([ + { + text: applyScoreColor({ score, text: '●' }), + width: 2, + padding: [0, 1, 0, 0], + }, + { + text: title, + // eslint-disable-next-line no-magic-numbers + padding: [0, 3, 0, 0], + }, + ...(value + ? [ + { + text: cyanBright(value), + // eslint-disable-next-line no-magic-numbers + width: 20, + padding: [0, 0, 0, 0], + }, + ] + : []), + ]); +} + export function logCategories({ categories, plugins }: ScoredReport): void { const hAlign = (idx: number) => (idx === 0 ? 'left' : 'right'); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts index 2884b17ba..0dd80be0c 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts @@ -234,7 +234,7 @@ describe('logPlugins', () => { false, ); const output = logs.join('\n'); - expect(output).toContain('All audits have perfect scores'); + expect(output).toContain('All 2 audits have perfect scores'); }); it('should log original audits when verbose is false and no audits have perfect scores', () => {