diff --git a/package-lock.json b/package-lock.json index 5e1051211..5c6ca00b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@code-pushup/portal-client": "^0.8.0", + "@code-pushup/portal-client": "^0.9.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "17.3.2", "@poppinss/cliui": "^6.4.0", @@ -2125,9 +2125,9 @@ } }, "node_modules/@code-pushup/portal-client": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.8.0.tgz", - "integrity": "sha512-JzmpFqkbyypN9VNzbfZ7QheYPnQhgBMWUfXGuKfCiTZbG9GCP3PurNpKkCxNJRVmSpd/czS8pP6A5soa1pkByQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.9.0.tgz", + "integrity": "sha512-ABQlc6x24UflEm7uO7Tt9KlU0NnhbmbAj7lUv7H9dgxgJ0fcK5R3rSp7hYab3npawb2nX+6+aG2OaPSiYsFPBQ==", "dependencies": { "graphql": "^16.6.0", "graphql-request": "^6.1.0", diff --git a/package.json b/package.json index 60d8a97e9..786fa0162 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "node": ">=18.16" }, "dependencies": { - "@code-pushup/portal-client": "^0.8.0", + "@code-pushup/portal-client": "^0.9.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "17.3.2", "@poppinss/cliui": "^6.4.0", diff --git a/packages/cli/src/lib/compare/compare-command.ts b/packages/cli/src/lib/compare/compare-command.ts index 0c19b0bea..880f6c53c 100644 --- a/packages/cli/src/lib/compare/compare-command.ts +++ b/packages/cli/src/lib/compare/compare-command.ts @@ -1,10 +1,10 @@ import { bold, gray } from 'ansis'; import { CommandModule } from 'yargs'; import { compareReportFiles } from '@code-pushup/core'; -import { PersistConfig } from '@code-pushup/models'; +import { PersistConfig, UploadConfig } from '@code-pushup/models'; import { ui } from '@code-pushup/utils'; import { CLI_NAME } from '../constants'; -import type { CompareOptions } from '../implementation/compare.model'; +import { CompareOptions } from '../implementation/compare.model'; import { yargsCompareOptionsDefinition } from '../implementation/compare.options'; export function yargsCompareCommandObject() { @@ -19,11 +19,16 @@ export function yargsCompareCommandObject() { const options = args as CompareOptions & { persist: Required; + upload?: UploadConfig; }; - const { before, after, persist } = options; + const { before, after, persist, upload } = options; - const outputPaths = await compareReportFiles({ before, after }, persist); + const outputPaths = await compareReportFiles( + { before, after }, + persist, + upload, + ); ui().logger.info( `Reports diff written to ${outputPaths diff --git a/packages/cli/src/lib/compare/compare-command.unit.test.ts b/packages/cli/src/lib/compare/compare-command.unit.test.ts index a633fd0d1..b2ece7b42 100644 --- a/packages/cli/src/lib/compare/compare-command.unit.test.ts +++ b/packages/cli/src/lib/compare/compare-command.unit.test.ts @@ -43,6 +43,7 @@ describe('compare-command', () => { filename: DEFAULT_PERSIST_FILENAME, format: DEFAULT_PERSIST_FORMAT, }, + expect.any(Object), ); }); diff --git a/packages/core/package.json b/packages/core/package.json index 4d793c5d0..06f3f3692 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,7 +5,7 @@ "dependencies": { "@code-pushup/models": "0.48.0", "@code-pushup/utils": "0.48.0", - "@code-pushup/portal-client": "^0.8.0", + "@code-pushup/portal-client": "^0.9.0", "ansis": "^3.3.0" }, "type": "commonjs", diff --git a/packages/core/src/lib/compare.ts b/packages/core/src/lib/compare.ts index f048e02e4..d06be5c9d 100644 --- a/packages/core/src/lib/compare.ts +++ b/packages/core/src/lib/compare.ts @@ -1,10 +1,15 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { + PortalOperationError, + getPortalComparisonLink, +} from '@code-pushup/portal-client'; import { type Format, type PersistConfig, Report, ReportsDiff, + type UploadConfig, reportSchema, } from '@code-pushup/models'; import { @@ -14,6 +19,7 @@ import { generateMdReportsDiff, readJsonFile, scoreReport, + ui, } from '@code-pushup/utils'; import { name as packageName, version } from '../../package.json'; import { @@ -26,6 +32,7 @@ import { export async function compareReportFiles( inputPaths: Diff, persistConfig: Required, + uploadConfig: UploadConfig | undefined, ): Promise { const { outputDir, filename, format } = persistConfig; @@ -40,10 +47,15 @@ export async function compareReportFiles( const reportsDiff = compareReports(reports); + const portalUrl = + uploadConfig && reportsDiff.commits && format.includes('md') + ? await fetchPortalComparisonLink(uploadConfig, reportsDiff.commits) + : undefined; + return Promise.all( format.map(async fmt => { const outputPath = join(outputDir, `${filename}-diff.${fmt}`); - const content = reportsDiffToFileContent(reportsDiff, fmt); + const content = reportsDiffToFileContent(reportsDiff, fmt, portalUrl); await ensureDirectoryExists(outputDir); await writeFile(outputPath, content); return outputPath; @@ -86,11 +98,39 @@ export function compareReports(reports: Diff): ReportsDiff { function reportsDiffToFileContent( reportsDiff: ReportsDiff, format: Format, + portalUrl: string | undefined, ): string { switch (format) { case 'json': return JSON.stringify(reportsDiff, null, 2); case 'md': - return generateMdReportsDiff(reportsDiff); + return generateMdReportsDiff(reportsDiff, portalUrl ?? undefined); + } +} + +async function fetchPortalComparisonLink( + uploadConfig: UploadConfig, + commits: NonNullable, +): Promise { + const { server, apiKey, organization, project } = uploadConfig; + try { + return await getPortalComparisonLink({ + server, + apiKey, + parameters: { + organization, + project, + before: commits.before.hash, + after: commits.after.hash, + }, + }); + } catch (error) { + if (error instanceof PortalOperationError) { + ui().logger.warning( + `Failed to fetch portal comparison link - ${error.message}`, + ); + return undefined; + } + throw error; } } diff --git a/packages/core/src/lib/compare.unit.test.ts b/packages/core/src/lib/compare.unit.test.ts index 05ab40580..72015ab6d 100644 --- a/packages/core/src/lib/compare.unit.test.ts +++ b/packages/core/src/lib/compare.unit.test.ts @@ -1,5 +1,7 @@ import { vol } from 'memfs'; +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { getPortalComparisonLink } from '@code-pushup/portal-client'; import { Commit, Report, reportsDiffSchema } from '@code-pushup/models'; import { COMMIT_ALT_MOCK, @@ -13,6 +15,11 @@ import { Diff, fileExists, readJsonFile } from '@code-pushup/utils'; import { compareReportFiles, compareReports } from './compare'; describe('compareReportFiles', () => { + const commitShas = { + before: MINIMAL_REPORT_MOCK.commit!.hash, + after: REPORT_MOCK.commit!.hash, + }; + beforeEach(() => { vol.fromJSON( { @@ -30,6 +37,7 @@ describe('compareReportFiles', () => { after: join(MEMFS_VOLUME, 'target-report.json'), }, { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json'] }, + undefined, ); const reportsDiffPromise = readJsonFile( @@ -48,6 +56,7 @@ describe('compareReportFiles', () => { after: join(MEMFS_VOLUME, 'target-report.json'), }, { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json', 'md'] }, + undefined, ); await expect( @@ -57,6 +66,116 @@ describe('compareReportFiles', () => { fileExists(join(MEMFS_VOLUME, 'report-diff.md')), ).resolves.toBeTruthy(); }); + + it('should include portal link (fetched using upload config) in Markdown file', async () => { + await compareReportFiles( + { + before: join(MEMFS_VOLUME, 'source-report.json'), + after: join(MEMFS_VOLUME, 'target-report.json'), + }, + { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json', 'md'] }, + { + server: 'https://api.code-pushup.dev/graphql', + apiKey: 'cp_XXXXX', + organization: 'dunder-mifflin', + project: 'website', + }, + ); + + await expect( + readFile(join(MEMFS_VOLUME, 'report-diff.md'), 'utf8'), + ).resolves.toContain( + `[๐Ÿ•ต๏ธ See full comparison in Code PushUp portal ๐Ÿ”](https://code-pushup.example.com/portal/dunder-mifflin/website/comparison/${commitShas.before}/${commitShas.after})`, + ); + + expect(getPortalComparisonLink).toHaveBeenCalledWith< + Parameters + >({ + server: 'https://api.code-pushup.dev/graphql', + apiKey: 'cp_XXXXX', + parameters: { + organization: 'dunder-mifflin', + project: 'website', + before: commitShas.before, + after: commitShas.after, + }, + }); + }); + + it('should not include portal link in Markdown if upload config is missing', async () => { + await compareReportFiles( + { + before: join(MEMFS_VOLUME, 'source-report.json'), + after: join(MEMFS_VOLUME, 'target-report.json'), + }, + { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json', 'md'] }, + undefined, + ); + + await expect( + readFile(join(MEMFS_VOLUME, 'report-diff.md'), 'utf8'), + ).resolves.not.toContain( + '[๐Ÿ•ต๏ธ See full comparison in Code PushUp portal ๐Ÿ”]', + ); + + expect(getPortalComparisonLink).not.toHaveBeenCalled(); + }); + + it('should not include portal link in Markdown if report has no associated commits', async () => { + vol.fromJSON( + { + 'source-report.json': JSON.stringify({ + ...MINIMAL_REPORT_MOCK, + commit: null, + } satisfies Report), + 'target-report.json': JSON.stringify(REPORT_MOCK), + }, + MEMFS_VOLUME, + ); + await compareReportFiles( + { + before: join(MEMFS_VOLUME, 'source-report.json'), + after: join(MEMFS_VOLUME, 'target-report.json'), + }, + { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json', 'md'] }, + { + server: 'https://api.code-pushup.dev/graphql', + apiKey: 'cp_XXXXX', + organization: 'dunder-mifflin', + project: 'website', + }, + ); + + await expect( + readFile(join(MEMFS_VOLUME, 'report-diff.md'), 'utf8'), + ).resolves.not.toContain( + '[๐Ÿ•ต๏ธ See full comparison in Code PushUp portal ๐Ÿ”]', + ); + + expect(getPortalComparisonLink).not.toHaveBeenCalled(); + }); + + it('should not fetch portal link if Markdown not included in formats', async () => { + await compareReportFiles( + { + before: join(MEMFS_VOLUME, 'source-report.json'), + after: join(MEMFS_VOLUME, 'target-report.json'), + }, + { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json'] }, + { + server: 'https://api.code-pushup.dev/graphql', + apiKey: 'cp_XXXXX', + organization: 'dunder-mifflin', + project: 'website', + }, + ); + + expect(getPortalComparisonLink).not.toHaveBeenCalled(); + + await expect( + fileExists(join(MEMFS_VOLUME, 'report-diff.md')), + ).resolves.toBeFalsy(); + }); }); describe('compareReports', () => { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index fbb6401f3..baacc6714 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -72,6 +72,7 @@ export { } from './lib/reports/flatten-plugins'; export { generateMdReport } from './lib/reports/generate-md-report'; export { generateMdReportsDiff } from './lib/reports/generate-md-reports-diff'; +export { loadReport } from './lib/reports/load-report'; export { logStdoutSummary } from './lib/reports/log-stdout-summary'; export { scoreReport } from './lib/reports/scoring'; export { sortReport } from './lib/reports/sorting'; @@ -80,11 +81,7 @@ export { ScoredGroup, ScoredReport, } from './lib/reports/types'; -export { - calcDuration, - compareIssueSeverity, - loadReport, -} from './lib/reports/utils'; +export { calcDuration, compareIssueSeverity } from './lib/reports/utils'; export { isSemver, normalizeSemver, sortSemvers } from './lib/semver'; export * from './lib/text-formats'; export { diff --git a/packages/utils/src/lib/reports/__snapshots__/report-diff-with-portal-link.md b/packages/utils/src/lib/reports/__snapshots__/report-diff-with-portal-link.md new file mode 100644 index 000000000..723893888 --- /dev/null +++ b/packages/utils/src/lib/reports/__snapshots__/report-diff-with-portal-link.md @@ -0,0 +1,42 @@ +# Code PushUp + +๐Ÿคจ Code PushUp report has both **improvements and regressions** โ€“ compared target commit 0123456789abcdef0123456789abcdef01234567 with source commit abcdef0123456789abcdef0123456789abcdef01. + +[๐Ÿ•ต๏ธ See full comparison in Code PushUp portal ๐Ÿ”](https://app.code-pushup.dev/portal/dunder-mifflin/website/comparison/abcdef0123456789abcdef0123456789abcdef01/0123456789abcdef0123456789abcdef01234567) + +## ๐Ÿท๏ธ Categories + +| ๐Ÿท๏ธ Category | โญ Previous score | โญ Current score | ๐Ÿ”„ Score change | +| :------------- | :--------------: | :-------------: | :--------------------------------------------------------------: | +| Bug prevention | ๐ŸŸก 68 | ๐ŸŸก **63** | ![โ†“ โˆ’5](https://img.shields.io/badge/%E2%86%93%20%E2%88%925-red) | +| Performance | ๐ŸŸข 92 | ๐ŸŸข **94** | ![โ†‘ +2](https://img.shields.io/badge/%E2%86%91%20%2B2-green) | +| Code style | ๐ŸŸก 54 | ๐ŸŸก **54** | โ€“ | + +## ๐Ÿ—ƒ๏ธ Groups + +
+๐Ÿ‘ 1 group improved + +| ๐Ÿ”Œ Plugin | ๐Ÿ—ƒ๏ธ Group | โญ Previous score | โญ Current score | ๐Ÿ”„ Score change | +| :--------- | :---------- | :--------------: | :-------------: | :----------------------------------------------------------: | +| Lighthouse | Performance | ๐ŸŸข 92 | ๐ŸŸข **94** | ![โ†‘ +2](https://img.shields.io/badge/%E2%86%91%20%2B2-green) | + +1 other group is unchanged. + +
+ +## ๐Ÿ›ก๏ธ Audits + +
+๐Ÿ‘ 3 audits improved, ๐Ÿ‘Ž 1 audit regressed + +| ๐Ÿ”Œ Plugin | ๐Ÿ›ก๏ธ Audit | ๐Ÿ“ Previous value | ๐Ÿ“ Current value | ๐Ÿ”„ Value change | +| :----------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------- | :---------------: | :--------------: | :------------------------------------------------------------------------------: | +| [ESLint](https://www.npmjs.com/package/@code-pushup/eslint-plugin) | [Disallow unused variables](https://eslint.org/docs/latest/rules/no-unused-vars) | ๐ŸŸฉ passed | ๐ŸŸฅ **1 error** | ![โ†‘ +โˆžโ€‰%](https://img.shields.io/badge/%E2%86%91%20%2B%E2%88%9E%E2%80%89%25-red) | +| Lighthouse | [Largest Contentful Paint](https://developer.chrome.com/docs/lighthouse/performance/largest-contentful-paint/) | ๐ŸŸจ 1.5 s | ๐ŸŸจ **1.4 s** | ![โ†“ โˆ’8โ€‰%](https://img.shields.io/badge/%E2%86%93%20%E2%88%928%E2%80%89%25-green) | +| Lighthouse | [First Contentful Paint](https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint/) | ๐ŸŸจ 1.2 s | ๐ŸŸจ **1.1 s** | ![โ†“ โˆ’4โ€‰%](https://img.shields.io/badge/%E2%86%93%20%E2%88%924%E2%80%89%25-green) | +| Lighthouse | [Speed Index](https://developer.chrome.com/docs/lighthouse/performance/speed-index/) | ๐ŸŸฉ 1.2 s | ๐ŸŸฉ **1.1 s** | ![โ†“ โˆ’4โ€‰%](https://img.shields.io/badge/%E2%86%93%20%E2%88%924%E2%80%89%25-green) | + +48 other audits are unchanged. + +
diff --git a/packages/utils/src/lib/reports/generate-md-reports-diff.integration.test.ts b/packages/utils/src/lib/reports/generate-md-reports-diff.integration.test.ts index e511c0994..bc73a7397 100644 --- a/packages/utils/src/lib/reports/generate-md-reports-diff.integration.test.ts +++ b/packages/utils/src/lib/reports/generate-md-reports-diff.integration.test.ts @@ -58,4 +58,17 @@ describe('generateMdReportsDiff', () => { generateMdReportsDiff(reportsDiffAddedPluginMock()), ).toMatchFileSnapshot('__snapshots__/report-diff-added.md'); }); + + it('should format Markdown comment with link to portal', async () => { + const report = reportsDiffAltMock(); + const shas = [ + report.commits!.before.hash, + report.commits!.after.hash, + ] as const; + const portalUrl = `https://app.code-pushup.dev/portal/dunder-mifflin/website/comparison/${shas[0]}/${shas[1]}`; + + await expect(generateMdReportsDiff(report, portalUrl)).toMatchFileSnapshot( + '__snapshots__/report-diff-with-portal-link.md', + ); + }); }); diff --git a/packages/utils/src/lib/reports/generate-md-reports-diff.ts b/packages/utils/src/lib/reports/generate-md-reports-diff.ts index d0de96dbe..571c92194 100644 --- a/packages/utils/src/lib/reports/generate-md-reports-diff.ts +++ b/packages/utils/src/lib/reports/generate-md-reports-diff.ts @@ -5,26 +5,28 @@ import { TableRow, md, } from 'build-md'; -import { AuditDiff, ReportsDiff } from '@code-pushup/models'; +import { ReportsDiff } from '@code-pushup/models'; import { pluralize, pluralizeToken } from '../formatting'; import { HIERARCHY } from '../text-formats'; import { objectToEntries } from '../transform'; import { DiffOutcome } from './types'; import { - colorByScoreDiff, - formatDiffNumber, + formatScoreChange, formatScoreWithColor, - getDiffMarker, + formatValueChange, scoreMarker, } from './utils'; // to prevent exceeding Markdown comment character limit const MAX_ROWS = 100; -export function generateMdReportsDiff(diff: ReportsDiff): string { +export function generateMdReportsDiff( + diff: ReportsDiff, + portalUrl?: string, +): string { return new MarkdownDocument() .$concat( - createDiffHeaderSection(diff), + createDiffHeaderSection(diff, portalUrl), createDiffCategoriesSection(diff), createDiffGroupsSection(diff), createDiffAuditsSection(diff), @@ -32,7 +34,10 @@ export function generateMdReportsDiff(diff: ReportsDiff): string { .toString(); } -function createDiffHeaderSection(diff: ReportsDiff): MarkdownDocument { +function createDiffHeaderSection( + diff: ReportsDiff, + portalUrl: string | undefined, +): MarkdownDocument { const outcomeTexts = { positive: md`๐Ÿฅณ Code PushUp report has ${md.bold('improved')}`, negative: md`๐Ÿ˜Ÿ Code PushUp report has ${md.bold('regressed')}`, @@ -58,6 +63,10 @@ function createDiffHeaderSection(diff: ReportsDiff): MarkdownDocument { diff.commits ? md`${outcomeTexts[outcome]} โ€“ ${styleCommits(diff.commits)}.` : outcomeTexts[outcome], + ) + .paragraph( + portalUrl && + md.link(portalUrl, '๐Ÿ•ต๏ธ See full comparison in Code PushUp portal ๐Ÿ”'), ); } @@ -198,28 +207,6 @@ function createGroupsOrAuditsDetails( ); } -function formatScoreChange(diff: number): InlineText { - const marker = getDiffMarker(diff); - const text = formatDiffNumber(Math.round(diff * 1000) / 10); // round with max 1 decimal - return colorByScoreDiff(`${marker} ${text}`, diff); -} - -function formatValueChange({ - values, - scores, -}: Pick): InlineText { - const marker = getDiffMarker(values.diff); - const percentage = - values.before === 0 - ? values.diff > 0 - ? Number.POSITIVE_INFINITY - : Number.NEGATIVE_INFINITY - : Math.round((100 * values.diff) / values.before); - // eslint-disable-next-line no-irregular-whitespace - const text = `${formatDiffNumber(percentage)}โ€‰%`; - return colorByScoreDiff(`${marker} ${text}`, scores.diff); -} - function summarizeUnchanged( token: 'category' | 'group' | 'audit', { changed, unchanged }: { changed: unknown[]; unchanged: unknown[] }, diff --git a/packages/utils/src/lib/reports/load-report.ts b/packages/utils/src/lib/reports/load-report.ts new file mode 100644 index 000000000..1b09bc153 --- /dev/null +++ b/packages/utils/src/lib/reports/load-report.ts @@ -0,0 +1,32 @@ +import { join } from 'node:path'; +import { + Format, + PersistConfig, + Report, + reportSchema, +} from '@code-pushup/models'; +import { + ensureDirectoryExists, + readJsonFile, + readTextFile, +} from '../file-system'; + +type LoadedReportFormat = T extends 'json' ? Report : string; + +export async function loadReport( + options: Required> & { + format: T; + }, +): Promise> { + const { outputDir, filename, format } = options; + await ensureDirectoryExists(outputDir); + const filePath = join(outputDir, `${filename}.${format}`); + + if (format === 'json') { + const content = await readJsonFile(filePath); + return reportSchema.parse(content) as LoadedReportFormat; + } + + const text = await readTextFile(filePath); + return text as LoadedReportFormat; +} diff --git a/packages/utils/src/lib/reports/load-report.unit.test.ts b/packages/utils/src/lib/reports/load-report.unit.test.ts new file mode 100644 index 000000000..b0a4085bc --- /dev/null +++ b/packages/utils/src/lib/reports/load-report.unit.test.ts @@ -0,0 +1,63 @@ +import { vol } from 'memfs'; +import { Report } from '@code-pushup/models'; +import { MEMFS_VOLUME, REPORT_MOCK, reportMock } from '@code-pushup/test-utils'; +import { loadReport } from './load-report'; + +describe('loadReport', () => { + it('should load a valid JSON report', async () => { + vol.fromJSON( + { + [`report.json`]: JSON.stringify(reportMock()), + [`report.md`]: 'test-42', + }, + MEMFS_VOLUME, + ); + + await expect( + loadReport({ + outputDir: MEMFS_VOLUME, + filename: 'report', + format: 'json', + }), + ).resolves.toEqual(reportMock()); + }); + + it('should load a markdown file', async () => { + vol.fromJSON( + { + [`report.dummy.md`]: 'test-7', + [`report.json`]: '{"test":42}', + [`report.md`]: 'test-42', + }, + MEMFS_VOLUME, + ); + + await expect( + loadReport({ + outputDir: MEMFS_VOLUME, + format: 'md', + filename: 'report', + }), + ).resolves.toBe('test-42'); + }); + + it('should throw for an invalid JSON report', async () => { + vol.fromJSON( + { + [`report.json`]: JSON.stringify({ + ...REPORT_MOCK, + plugins: [{ ...REPORT_MOCK.plugins[0]!, slug: '-Invalid_slug' }], + } satisfies Report), + }, + MEMFS_VOLUME, + ); + + await expect( + loadReport({ + outputDir: MEMFS_VOLUME, + filename: 'report', + format: 'json', + }), + ).rejects.toThrow('slug has to follow the pattern'); + }); +}); diff --git a/packages/utils/src/lib/reports/utils.ts b/packages/utils/src/lib/reports/utils.ts index ae6062728..4fc7b7619 100644 --- a/packages/utils/src/lib/reports/utils.ts +++ b/packages/utils/src/lib/reports/utils.ts @@ -1,21 +1,12 @@ import { InlineText, md } from 'build-md'; -import { join } from 'node:path'; import { + AuditDiff, AuditReport, CategoryRef, IssueSeverity as CliIssueSeverity, - Format, Group, Issue, - PersistConfig, - Report, - reportSchema, } from '@code-pushup/models'; -import { - ensureDirectoryExists, - readJsonFile, - readTextFile, -} from '../file-system'; import { SCORE_COLOR_RANGE } from './constants'; import { ScoredReport, SortableAuditReport, SortableGroup } from './types'; @@ -105,6 +96,28 @@ export function severityMarker(severity: 'info' | 'warning' | 'error'): string { return 'โ„น๏ธ'; } +export function formatScoreChange(diff: number): InlineText { + const marker = getDiffMarker(diff); + const text = formatDiffNumber(Math.round(diff * 1000) / 10); // round with max 1 decimal + return colorByScoreDiff(`${marker} ${text}`, diff); +} + +export function formatValueChange({ + values, + scores, +}: Pick): InlineText { + const marker = getDiffMarker(values.diff); + const percentage = + values.before === 0 + ? values.diff > 0 + ? Number.POSITIVE_INFINITY + : Number.NEGATIVE_INFINITY + : Math.round((100 * values.diff) / values.before); + // eslint-disable-next-line no-irregular-whitespace + const text = `${formatDiffNumber(percentage)}โ€‰%`; + return colorByScoreDiff(`${marker} ${text}`, scores.diff); +} + export function calcDuration(start: number, stop?: number): number { return Math.round((stop ?? performance.now()) - start); } @@ -261,26 +274,6 @@ export function compareIssueSeverity( return levels[severity1] - levels[severity2]; } -type LoadedReportFormat = T extends 'json' ? Report : string; - -export async function loadReport( - options: Required> & { - format: T; - }, -): Promise> { - const { outputDir, filename, format } = options; - await ensureDirectoryExists(outputDir); - const filePath = join(outputDir, `${filename}.${format}`); - - if (format === 'json') { - const content = await readJsonFile(filePath); - return reportSchema.parse(content) as LoadedReportFormat; - } - - const text = await readTextFile(filePath); - return text as LoadedReportFormat; -} - export function throwIsNotPresentError( itemName: string, presentPlace: string, diff --git a/packages/utils/src/lib/reports/utils.unit.test.ts b/packages/utils/src/lib/reports/utils.unit.test.ts index 89da73134..f3100fdd9 100644 --- a/packages/utils/src/lib/reports/utils.unit.test.ts +++ b/packages/utils/src/lib/reports/utils.unit.test.ts @@ -1,7 +1,5 @@ -import { vol } from 'memfs'; import { describe, expect, it } from 'vitest'; -import { AuditReport, Issue, IssueSeverity, Report } from '@code-pushup/models'; -import { MEMFS_VOLUME, REPORT_MOCK, reportMock } from '@code-pushup/test-utils'; +import { AuditReport, Issue, IssueSeverity } from '@code-pushup/models'; import { SCORE_COLOR_RANGE } from './constants'; import { ScoredReport, SortableAuditReport, SortableGroup } from './types'; import { @@ -21,7 +19,6 @@ import { getSortableAuditByRef, getSortableGroupByRef, getSortedGroupAudits, - loadReport, scoreMarker, severityMarker, } from './utils'; @@ -367,65 +364,6 @@ describe('compareIssueSeverity', () => { }); }); -describe('loadReport', () => { - it('should load a valid JSON report', async () => { - vol.fromJSON( - { - [`report.json`]: JSON.stringify(reportMock()), - [`report.md`]: 'test-42', - }, - MEMFS_VOLUME, - ); - - await expect( - loadReport({ - outputDir: MEMFS_VOLUME, - filename: 'report', - format: 'json', - }), - ).resolves.toEqual(reportMock()); - }); - - it('should load a markdown file', async () => { - vol.fromJSON( - { - [`report.dummy.md`]: 'test-7', - [`report.json`]: '{"test":42}', - [`report.md`]: 'test-42', - }, - MEMFS_VOLUME, - ); - - await expect( - loadReport({ - outputDir: MEMFS_VOLUME, - format: 'md', - filename: 'report', - }), - ).resolves.toBe('test-42'); - }); - - it('should throw for an invalid JSON report', async () => { - vol.fromJSON( - { - [`report.json`]: JSON.stringify({ - ...REPORT_MOCK, - plugins: [{ ...REPORT_MOCK.plugins[0]!, slug: '-Invalid_slug' }], - } satisfies Report), - }, - MEMFS_VOLUME, - ); - - await expect( - loadReport({ - outputDir: MEMFS_VOLUME, - filename: 'report', - format: 'json', - }), - ).rejects.toThrow('slug has to follow the pattern'); - }); -}); - describe('compareCategoryAuditsAndGroups', () => { it('should sort audits by weight and score', () => { const mockAudits = [ diff --git a/testing/test-setup/src/lib/portal-client.mock.ts b/testing/test-setup/src/lib/portal-client.mock.ts index c7b062121..54dec2d05 100644 --- a/testing/test-setup/src/lib/portal-client.mock.ts +++ b/testing/test-setup/src/lib/portal-client.mock.ts @@ -1,5 +1,6 @@ import { vi } from 'vitest'; import type { + PortalComparisonLinkArgs, PortalUploadArgs, ReportFragment, } from '@code-pushup/portal-client'; @@ -15,5 +16,9 @@ vi.mock('@code-pushup/portal-client', async () => { url: `https://code-pushup.example.com/portal/${data.organization}/${data.project}/commit/${data.commit}`, }), ), + getPortalComparisonLink: vi.fn( + async ({ parameters }: PortalComparisonLinkArgs) => + `https://code-pushup.example.com/portal/${parameters.organization}/${parameters.project}/comparison/${parameters.before}/${parameters.after}`, + ), }; });