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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Dark
- Light
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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) =>`
-
- s
-
@@ -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
});
}