diff --git a/packages/utils/src/lib/reports/__snapshots__/generate-md-report-category-section.unit.test.ts.snap b/packages/utils/src/lib/reports/__snapshots__/generate-md-report-category-section.unit.test.ts.snap index d8b4fde36..191eaf9ab 100644 --- a/packages/utils/src/lib/reports/__snapshots__/generate-md-report-category-section.unit.test.ts.snap +++ b/packages/utils/src/lib/reports/__snapshots__/generate-md-report-category-section.unit.test.ts.snap @@ -45,6 +45,17 @@ exports[`categoriesDetailsSection > should render complete categories details 1` " `; +exports[`categoriesDetailsSection > should render filtered categories details > filtered 1`] = ` +"## 🏷 Categories + +### Bug Prevention + +🟢 Score: **100** ✅ + +- 🟩 [No let](#no-let-eslint) (_Eslint_) - **0** +" +`; + exports[`categoriesOverviewSection > should render complete categories table 1`] = ` "| 🏷 Category | ⭐ Score | 🛡 Audits | | :-------------------------------- | :-------: | :-------: | @@ -54,6 +65,13 @@ exports[`categoriesOverviewSection > should render complete categories table 1`] " `; +exports[`categoriesOverviewSection > should render filtered categories table 1`] = ` +"| 🏷 Category | ⭐ Score | 🛡 Audits | +| :-------------------------------- | :--------: | :-------: | +| [Bug Prevention](#bug-prevention) | 🟢 **100** | 1 | +" +`; + exports[`categoriesOverviewSection > should render targetScore icon "❌" if score fails 1`] = ` "| 🏷 Category | ⭐ Score | 🛡 Audits | | :-------------------------------- | :---------: | :-------: | diff --git a/packages/utils/src/lib/reports/generate-md-report-categoy-section.ts b/packages/utils/src/lib/reports/generate-md-report-category-section.ts similarity index 78% rename from packages/utils/src/lib/reports/generate-md-report-categoy-section.ts rename to packages/utils/src/lib/reports/generate-md-report-category-section.ts index 063bc30e3..5cac4a477 100644 --- a/packages/utils/src/lib/reports/generate-md-report-categoy-section.ts +++ b/packages/utils/src/lib/reports/generate-md-report-category-section.ts @@ -4,17 +4,19 @@ import { slugify } from '../formatting.js'; import { HIERARCHY } from '../text-formats/index.js'; import { metaDescription } from './formatting.js'; import { getSortableAuditByRef, getSortableGroupByRef } from './sorting.js'; -import type { ScoredGroup, ScoredReport } from './types.js'; +import type { ScoreFilter, ScoredGroup, ScoredReport } from './types.js'; import { countCategoryAudits, formatReportScore, getPluginNameFromSlug, + scoreFilter, scoreMarker, targetScoreIcon, } from './utils.js'; export function categoriesOverviewSection( report: Required>, + options?: ScoreFilter, ): MarkdownDocument { const { categories, plugins } = report; return new MarkdownDocument().table( @@ -23,26 +25,29 @@ export function categoriesOverviewSection( { heading: '⭐ Score', alignment: 'center' }, { heading: '🛡 Audits', alignment: 'center' }, ], - categories.map(({ title, refs, score, isBinary }) => [ - // @TODO refactor `isBinary: boolean` to `targetScore: number` #713 - // The heading "ID" is inferred from the heading text in Markdown. - md.link(`#${slugify(title)}`, title), - md`${scoreMarker(score)} ${md.bold( - formatReportScore(score), - )}${binaryIconSuffix(score, isBinary)}`, - countCategoryAudits(refs, plugins).toString(), - ]), + categories + .filter(scoreFilter(options)) + .map(({ title, refs, score, isBinary }) => [ + // @TODO refactor `isBinary: boolean` to `targetScore: number` #713 + // The heading "ID" is inferred from the heading text in Markdown. + md.link(`#${slugify(title)}`, title), + md`${scoreMarker(score)} ${md.bold( + formatReportScore(score), + )}${binaryIconSuffix(score, isBinary)}`, + countCategoryAudits(refs, plugins).toString(), + ]), ); } export function categoriesDetailsSection( report: Required>, + options?: ScoreFilter, ): MarkdownDocument { const { categories, plugins } = report; - + const isScoreDisplayed = scoreFilter(options); return new MarkdownDocument() .heading(HIERARCHY.level_2, '🏷 Categories') - .$foreach(categories, (doc, category) => + .$foreach(categories.filter(isScoreDisplayed), (doc, category) => doc .heading(HIERARCHY.level_3, category.title) .paragraph(metaDescription(category)) @@ -63,13 +68,17 @@ export function categoriesDetailsSection( ), ); const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins); - return categoryGroupItem(group, groupAudits, pluginTitle); + return isScoreDisplayed(group) + ? categoryGroupItem(group, groupAudits, pluginTitle) + : ''; } // Add audit details else { const audit = getSortableAuditByRef(ref, plugins); const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins); - return categoryRef(audit, pluginTitle); + return isScoreDisplayed(audit) + ? categoryRef(audit, pluginTitle) + : ''; } }), ), diff --git a/packages/utils/src/lib/reports/generate-md-report-category-section.unit.test.ts b/packages/utils/src/lib/reports/generate-md-report-category-section.unit.test.ts index 6afaca2f2..01b00876e 100644 --- a/packages/utils/src/lib/reports/generate-md-report-category-section.unit.test.ts +++ b/packages/utils/src/lib/reports/generate-md-report-category-section.unit.test.ts @@ -6,7 +6,7 @@ import { categoriesOverviewSection, categoryGroupItem, categoryRef, -} from './generate-md-report-categoy-section.js'; +} from './generate-md-report-category-section.js'; import type { ScoredGroup, ScoredReport } from './types.js'; // === Categories Overview Section @@ -49,6 +49,48 @@ describe('categoriesOverviewSection', () => { ).toMatchSnapshot(); }); + it('should render filtered categories table', () => { + expect( + categoriesOverviewSection( + { + plugins: [ + { + slug: 'eslint', + title: 'Eslint', + }, + { + slug: 'lighthouse', + title: 'Lighthouse', + }, + ], + categories: [ + { + slug: 'bug-prevention', + title: 'Bug Prevention', + score: 1, + refs: [{ slug: 'no-let', type: 'audit' }], + }, + { + slug: 'performance', + title: 'Performance', + score: 0.74, + refs: [{ slug: 'largest-contentful-paint', type: 'audit' }], + }, + { + slug: 'typescript', + title: 'Typescript', + score: 0.14, + refs: [{ slug: 'no-any', type: 'audit' }], + }, + ], + } as Required>, + { + isScoreListed: score => score === 1, + }, + ).toString(), + ).toMatchSnapshot(); + }); + it('should render targetScore icon "❌" if score fails', () => { expect( categoriesOverviewSection({ @@ -215,6 +257,68 @@ describe('categoriesDetailsSection', () => { ).toMatchSnapshot(); }); + it('should render filtered categories details', () => { + expect( + categoriesDetailsSection( + { + plugins: [ + { + slug: 'eslint', + title: 'Eslint', + audits: [ + { slug: 'no-let', title: 'No let', score: 1, value: 0 }, + { slug: 'no-any', title: 'No any', score: 0, value: 5 }, + ], + }, + { + slug: 'lighthouse', + title: 'Lighthouse', + audits: [ + { + slug: 'largest-contentful-paint', + title: 'Largest Contentful Paint', + score: 0.7, + value: 2905, + }, + ], + }, + ], + categories: [ + { + slug: 'bug-prevention', + title: 'Bug Prevention', + score: 1, + isBinary: true, + refs: [{ slug: 'no-let', type: 'audit', plugin: 'eslint' }], + }, + { + slug: 'performance', + title: 'Performance', + score: 0.74, + refs: [ + { + slug: 'largest-contentful-paint', + type: 'audit', + plugin: 'lighthouse', + }, + ], + }, + { + slug: 'typescript', + title: 'Typescript', + score: 0.14, + isBinary: true, + refs: [{ slug: 'no-any', type: 'audit', plugin: 'eslint' }], + }, + ], + } as Required>, + { + isScoreListed: score => score === 1, + }, + ).toString(), + ).toMatchSnapshot('filtered'); + }); + it('should render categories details and add "❌" when isBinary is failing', () => { expect( categoriesDetailsSection({ diff --git a/packages/utils/src/lib/reports/generate-md-report.ts b/packages/utils/src/lib/reports/generate-md-report.ts index 3ae8b1c7d..c452b22f6 100644 --- a/packages/utils/src/lib/reports/generate-md-report.ts +++ b/packages/utils/src/lib/reports/generate-md-report.ts @@ -16,9 +16,14 @@ import { import { categoriesDetailsSection, categoriesOverviewSection, -} from './generate-md-report-categoy-section.js'; +} from './generate-md-report-category-section.js'; import type { MdReportOptions, ScoredReport } from './types.js'; -import { formatReportScore, scoreMarker, severityMarker } from './utils.js'; +import { + formatReportScore, + scoreFilter, + scoreMarker, + severityMarker, +} from './utils.js'; export function auditDetailsAuditValue({ score, @@ -30,6 +35,10 @@ export function auditDetailsAuditValue({ )} (score: ${formatReportScore(score)})`; } +/** + * Check if the report has categories. + * @param report + */ function hasCategories( report: ScoredReport, ): report is ScoredReport & Required> { @@ -44,7 +53,10 @@ export function generateMdReport( .heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT) .$concat( ...(hasCategories(report) - ? [categoriesOverviewSection(report), categoriesDetailsSection(report)] + ? [ + categoriesOverviewSection(report, options), + categoriesDetailsSection(report, options), + ] : []), auditsSection(report, options), aboutSection(report), @@ -110,11 +122,14 @@ export function auditsSection( { plugins }: Pick, options?: MdReportOptions, ): MarkdownDocument { + const isScoreDisplayed = scoreFilter(options); return new MarkdownDocument() .heading(HIERARCHY.level_2, '🛡️ Audits') .$foreach( plugins.flatMap(plugin => - plugin.audits.map(audit => ({ ...audit, plugin })), + plugin.audits + .filter(isScoreDisplayed) + .map(audit => ({ ...audit, plugin })), ), (doc, { plugin, ...audit }) => { const auditTitle = `${audit.title} (${plugin.title})`; diff --git a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts index bc870837f..671f6b4b8 100644 --- a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts +++ b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts @@ -47,6 +47,54 @@ const baseScoredReport = { ], } as ScoredReport; +const baseScoredReport2 = { + date: '2025.01.01', + duration: 4200, + version: 'v1.0.0', + commit: { + message: 'ci: update action', + author: 'Michael ', + date: new Date('2025.01.01'), + hash: '535b8e9e557336618a764f3fa45609d224a62837', + }, + plugins: [ + { + slug: 'lighthouse', + version: '1.0.1', + duration: 15_365, + title: 'Lighthouse', + audits: [ + { + slug: 'largest-contentful-paint', + title: 'Largest Contentful Paint', + score: 0.6, + value: 2700, + }, + { + slug: 'cumulative-layout-shift', + title: 'Cumulative Layout Shift', + score: 1, + value: 0, + }, + ], + }, + ], + categories: [ + { + title: 'Speed', + slug: 'speed', + score: 0.93, + refs: [{ slug: 'largest-contentful-paint', plugin: 'lighthouse' }], + }, + { + title: 'Visual Stability', + slug: 'visual-stability', + score: 1, + refs: [{ slug: 'cumulative-layout-shift', plugin: 'lighthouse' }], + }, + ], +} as ScoredReport; + // === Audit Details describe('auditDetailsAuditValue', () => { @@ -359,6 +407,22 @@ describe('auditsSection', () => { ).toMatch('🟩 **0** (score: 100)'); }); + it('should render filtered result', () => { + const auditSection = auditsSection( + { + plugins: [ + { audits: [{ score: 1, value: 0 }] }, + { audits: [{ score: 0, value: 1 }] }, + ], + } as ScoredReport, + { + isScoreListed: (score: number) => score === 1, + }, + ).toString(); + expect(auditSection).toMatch('(score: 100)'); + expect(auditSection).not.toMatch('(score: 0)'); + }); + it('should render audit details', () => { const md = auditsSection({ plugins: [ @@ -580,6 +644,18 @@ describe('generateMdReport', () => { expect(md).toMatch('Made with ❤ by [Code PushUp]'); }); + it('should render sections filtered by isScoreListed of the report', () => { + const md = generateMdReport(baseScoredReport2, { + isScoreListed: (score: number) => score === 1, + }); + + expect(md).toMatch('Visual Stability'); + expect(md).toMatch('Cumulative Layout Shift'); + + expect(md).not.toMatch('Speed'); + expect(md).not.toMatch('Largest Contentful Paint'); + }); + it('should skip categories section when categories are missing', () => { const md = generateMdReport({ ...baseScoredReport, categories: undefined }); expect(md).not.toMatch('## 🏷 Categories'); diff --git a/packages/utils/src/lib/reports/types.ts b/packages/utils/src/lib/reports/types.ts index 1c89d5986..a79059db0 100644 --- a/packages/utils/src/lib/reports/types.ts +++ b/packages/utils/src/lib/reports/types.ts @@ -32,7 +32,11 @@ export type SortableAuditReport = AuditReport & { export type DiffOutcome = 'positive' | 'negative' | 'mixed' | 'unchanged'; -export type MdReportOptions = Pick; +export type ScoreFilter = { + isScoreListed?: (score: number) => boolean; +}; + +export type MdReportOptions = Pick & ScoreFilter; export const SUPPORTED_ENVIRONMENTS = [ 'vscode', diff --git a/packages/utils/src/lib/reports/utils.ts b/packages/utils/src/lib/reports/utils.ts index 7dfaba833..fbb0aec0f 100644 --- a/packages/utils/src/lib/reports/utils.ts +++ b/packages/utils/src/lib/reports/utils.ts @@ -10,11 +10,19 @@ import type { } from '@code-pushup/models'; import { SCORE_COLOR_RANGE } from './constants.js'; import type { + ScoreFilter, ScoredReport, SortableAuditReport, SortableGroup, } from './types.js'; +export function scoreFilter( + options?: ScoreFilter, +) { + const { isScoreListed = () => true } = options ?? {}; + return ({ score }: T) => isScoreListed(score); +} + export function formatReportScore(score: number): string { const scaledScore = score * 100; const roundedScore = Math.round(scaledScore); diff --git a/packages/utils/src/lib/reports/utils.unit.test.ts b/packages/utils/src/lib/reports/utils.unit.test.ts index 965807a35..087edb82a 100644 --- a/packages/utils/src/lib/reports/utils.unit.test.ts +++ b/packages/utils/src/lib/reports/utils.unit.test.ts @@ -26,11 +26,24 @@ import { formatValueChange, getPluginNameFromSlug, roundValue, + scoreFilter, scoreMarker, severityMarker, targetScoreIcon, } from './utils.js'; +describe('scoreFilter', () => { + it('should not filter by score if no options are passed', () => { + expect(scoreFilter()({ score: 0 })).toBe(true); + }); + + it('should filter by score if options are passed', () => { + expect( + scoreFilter({ isScoreListed: score => score === 0.5 })({ score: 0 }), + ).toBe(false); + }); +}); + describe('formatReportScore', () => { it.each([ [0, '0'],