From bf38cc0f515e68972a571529a5a8ddadac935743 Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Tue, 2 Sep 2025 15:11:23 +0530 Subject: [PATCH] adding metadata to report (#5360) --- .../ReduxStore/slices/collections/index.js | 3 + packages/bruno-cli/src/commands/run.js | 3 +- .../src/reporters/html-template.html | 637 ------------------ packages/bruno-cli/src/reporters/html.js | 34 +- .../tests/runner/report-metadata.spec.js | 52 ++ .../runner/reports/html/generate-report.ts | 17 +- .../src/runner/reports/html/template.ts | 134 +++- .../bruno-electron/src/ipc/network/index.js | 7 +- 8 files changed, 227 insertions(+), 660 deletions(-) delete mode 100644 packages/bruno-cli/src/reporters/html-template.html create mode 100644 packages/bruno-cli/tests/runner/report-metadata.spec.js diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 2d6df3ec6..909726a2a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2440,6 +2440,9 @@ export const collectionsSlice = createSlice({ if (type === 'testrun-ended') { const info = collection.runnerResult.info; info.status = 'ended'; + if (action.payload.runCompletionTime) { + info.runCompletionTime = action.payload.runCompletionTime; + } if (action.payload.statusText) { info.statusText = action.payload.statusText; } diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index e3b7aa3ea..261244949 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -623,6 +623,7 @@ const handler = async function (argv) { } const summary = printRunSummary(results); + const runCompletionTime = new Date().toISOString(); const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0); console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`))); @@ -636,7 +637,7 @@ const handler = async function (argv) { const reporters = { 'json': (path) => fs.writeFileSync(path, JSON.stringify(outputJson, null, 2)), 'junit': (path) => makeJUnitOutput(results, path), - 'html': (path) => makeHtmlOutput(outputJson, path), + 'html': (path) => makeHtmlOutput(outputJson, path, runCompletionTime), } for (const formatter of Object.keys(formats)) diff --git a/packages/bruno-cli/src/reporters/html-template.html b/packages/bruno-cli/src/reporters/html-template.html deleted file mode 100644 index b4cbca0ac..000000000 --- a/packages/bruno-cli/src/reporters/html-template.html +++ /dev/null @@ -1,637 +0,0 @@ - - - - - - - - - Bruno - - - -
- - - - - - - - - - - - - - - - - - - - -
- - - - - - - diff --git a/packages/bruno-cli/src/reporters/html.js b/packages/bruno-cli/src/reporters/html.js index 7fb594224..0c7975cd3 100644 --- a/packages/bruno-cli/src/reporters/html.js +++ b/packages/bruno-cli/src/reporters/html.js @@ -1,13 +1,31 @@ const fs = require('fs'); -const path = require('path'); +const { generateHtmlReport } = require('@usebruno/common/runner'); +const { CLI_VERSION } = require('../constants'); -const makeHtmlOutput = async (results, outputPath) => { - const resultsJson = JSON.stringify(results, null, 2); - - const reportPath = path.join(__dirname, 'html-template.html'); - const template = fs.readFileSync(reportPath, 'utf8'); - - fs.writeFileSync(outputPath, template.replace('__RESULTS_JSON__', resultsJson)); +const makeHtmlOutput = async (results, outputPath, runCompletionTime) => { + let runnerResults = results; + if (!results) { + runnerResults = []; + } else if (results.results) { + // Convert CLI format to expected format: array of { iterationIndex, results, summary } + runnerResults = [{ + iterationIndex: 0, + results: results.results, + summary: results.summary + }]; + } else if (Array.isArray(results)) { + runnerResults = results; + } + + const environment = runnerResults.length > 0 ? runnerResults[0].environment : null; + + const htmlString = generateHtmlReport({ + runnerResults: runnerResults, + version: `usebruno v${CLI_VERSION}`, + environment: environment, + runCompletionTime: runCompletionTime + }); + fs.writeFileSync(outputPath, htmlString); }; module.exports = makeHtmlOutput; diff --git a/packages/bruno-cli/tests/runner/report-metadata.spec.js b/packages/bruno-cli/tests/runner/report-metadata.spec.js new file mode 100644 index 000000000..9732e4aeb --- /dev/null +++ b/packages/bruno-cli/tests/runner/report-metadata.spec.js @@ -0,0 +1,52 @@ +const { describe, it, expect } = require('@jest/globals'); +const { generateHtmlReport } = require('@usebruno/common/runner'); + +describe('HTML Report Generation', () => { + it('should include all metadata in the HTML report', async () => { + // Sample test results + const mockResults = [ + { + iterationIndex: 0, + environment: 'production', + results: [], + summary: { + totalRequests: 1, + passedRequests: 1, + failedRequests: 0, + errorRequests: 0, + skippedRequests: 0, + totalAssertions: 0, + passedAssertions: 0, + failedAssertions: 0, + totalTests: 0, + passedTests: 0, + failedTests: 0 + } + } + ]; + + // Generate HTML using mock data + const htmlString = generateHtmlReport({ + runnerResults: mockResults, + version: 'usebruno v1.16.0', + environment: 'production', + runCompletionTime: '2024-01-15T14:30:45.123Z' + }); + + // Verify the HTML contains expected metadata structure + expect(htmlString).toContain('Bruno run dashboard'); + expect(htmlString).toContain('Date & Time'); + expect(htmlString).toContain('Version'); + expect(htmlString).toContain('Environment'); + expect(htmlString).toContain('Total run duration'); + expect(htmlString).toContain('Total data received'); + expect(htmlString).toContain('Average response time'); + + expect(htmlString).toContain('{{ runCompletionTime }}'); + expect(htmlString).toContain('{{ brunoVersion }}'); + expect(htmlString).toContain('{{ environment }}'); + expect(htmlString).toContain('{{ totalDuration }}'); + expect(htmlString).toContain('{{ totalDataReceived }}'); + expect(htmlString).toContain('{{ averageResponseTime }}'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-common/src/runner/reports/html/generate-report.ts b/packages/bruno-common/src/runner/reports/html/generate-report.ts index 7309c483e..e08ddce29 100644 --- a/packages/bruno-common/src/runner/reports/html/generate-report.ts +++ b/packages/bruno-common/src/runner/reports/html/generate-report.ts @@ -3,9 +3,15 @@ import { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from import htmlTemplateString from "./template"; const generateHtmlReport = ({ - runnerResults + runnerResults, + version = '', // Default to empty string if not provided + environment = null, // Default environment if not provided + runCompletionTime = '' // Default run completion time if not provided }: { - runnerResults: T_RunnerResults[] + runnerResults: T_RunnerResults[]; + version?: string; + environment?: string | null; + runCompletionTime?: string; }): string => { const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results, summary }) => { return { @@ -31,7 +37,12 @@ const generateHtmlReport = ({ summary } }); - const htmlString = htmlTemplateString(encodeBase64(JSON.stringify(resultsWithSummaryAndCleanData))); + const htmlString = htmlTemplateString(encodeBase64(JSON.stringify({ + results: resultsWithSummaryAndCleanData, + version, + environment, + runCompletionTime + }))); return htmlString; }; diff --git a/packages/bruno-common/src/runner/reports/html/template.ts b/packages/bruno-common/src/runner/reports/html/template.ts index cd0839e67..44589b81d 100644 --- a/packages/bruno-common/src/runner/reports/html/template.ts +++ b/packages/bruno-common/src/runner/reports/html/template.ts @@ -28,6 +28,37 @@ export const htmlTemplateString = (resutsJsonString: string) =>` .min-width-150 { min-width: 150px; } + + /* Metadata card styling - minimal custom styles */ + .metadata-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 8px; + margin-top: 12px; + } + + .metadata-item { + text-align: center; + padding: 6px 8px; + border-radius: 6px; + display: flex; + flex-direction: column; + } + + .metadata-label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + opacity: 0.7; + } + + .metadata-value { + font-size: 0.8rem; + font-weight: normal; + word-wrap: break-word; + overflow-wrap: break-word; + } @@ -162,6 +193,35 @@ export const htmlTemplateString = (resutsJsonString: string) =>` + + + + @@ -213,12 +273,6 @@ export const htmlTemplateString = (resutsJsonString: string) =>` - - - @@ -400,10 +454,25 @@ export const htmlTemplateString = (resutsJsonString: string) =>` const bytes = Uint8Array.from(binary, c => c.charCodeAt(0)); return new TextDecoder().decode(bytes); } + const rawResults = JSON.parse(decodeBase64('${resutsJsonString}')); const res = computed(() => { - const rawResults = JSON.parse(decodeBase64('${resutsJsonString}')); - return mergeTests(rawResults); + return mergeTests(rawResults.results); + }); + + const brunoVersion = computed(() => { + return rawResults.version || '-'; + }); + + const environment = computed(() => { + return rawResults.environment || '-'; + }); + + const runCompletionTime = computed(() => { + if (rawResults.runCompletionTime) { + return new Date(rawResults.runCompletionTime).toLocaleString(); + } + return '-'; }); const currentTab = ref('summary'); @@ -422,6 +491,47 @@ export const htmlTemplateString = (resutsJsonString: string) =>` const theme = computed(() => { return darkMode.value ? naive.darkTheme : null; }); + + const totalDuration = computed(() => { + const total = res.value.reduce((totalTime, iteration) => { + return totalTime + iteration.results.reduce((sum, result) => sum + (result.runDuration || 0), 0); + }, 0); + return total > 0 ? Math.round(total * 1000) / 1000 + 's' : '-'; + }); + + const totalDataReceived = computed(() => { + const bytes = res.value.reduce((total, iteration) => { + return total + iteration.results.reduce((sum, result) => { + const responseData = result.response?.data; + if (typeof responseData === 'string') { + return sum + new Blob([responseData]).size; + } + return sum + (JSON.stringify(responseData || {}).length || 0); + }, 0); + }, 0); + + if (bytes === 0) return '-'; + if (bytes < 1024) return bytes + 'B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + 'KB'; + return (bytes / (1024 * 1024)).toFixed(2) + 'MB'; + }); + + const averageResponseTime = computed(() => { + let totalTime = 0; + let count = 0; + + res.value.forEach(iteration => { + iteration.results.forEach(result => { + if (result.response?.responseTime) { + totalTime += result.response.responseTime; + count++; + } + }); + }); + + return count > 0 ? Math.round(totalTime / count) + 'ms' : '-'; + }); + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { darkMode.value = true; } @@ -434,7 +544,13 @@ export const htmlTemplateString = (resutsJsonString: string) =>` theme, darkMode, darkModeRailStyle: () => ({ background: 'var(--n-rail-color)' }), - currentTab + currentTab, + brunoVersion, + environment, + totalDuration, + totalDataReceived, + averageResponseTime, + runCompletionTime }; } }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 37fd9bd3a..bf22f4f66 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1360,7 +1360,8 @@ const registerNetworkIpc = (mainWindow) => { type: 'testrun-ended', collectionUid, folderUid, - statusText: 'collection run was terminated!' + statusText: 'collection run was terminated!', + runCompletionTime: new Date().toISOString(), }); break; } @@ -1389,7 +1390,8 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:run-folder-event', { type: 'testrun-ended', collectionUid, - folderUid + folderUid, + runCompletionTime: new Date().toISOString(), }); } catch (error) { console.log("error", error); @@ -1398,6 +1400,7 @@ const registerNetworkIpc = (mainWindow) => { type: 'testrun-ended', collectionUid, folderUid, + runCompletionTime: new Date().toISOString(), error: error && !error.isCancel ? error : null }); }