diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js index 001b4dc292..fc993a63ca 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js @@ -1,6 +1,18 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + color: ${(props) => props.theme.text}; + + .test-summary { + transition: background-color 0.2s; + border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder}; + color: ${(props) => props.theme.text}; + + &:hover { + background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } + .test-success { color: ${(props) => props.theme.colors.text.green}; } @@ -9,12 +21,24 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.colors.text.danger}; } + .test-success-count { + color: ${(props) => props.theme.colors.text.green}; + } + + .test-failure-count { + color: ${(props) => props.theme.colors.text.danger}; + } + .error-message { color: ${(props) => props.theme.colors.text.muted}; } - .skipped-request { - color: ${(props) => props.theme.colors.text.muted}; + .test-results-list { + transition: all 0.3s ease; + } + + .dropdown-icon { + color: ${(props) => props.theme.sidebar.dropdownIcon.color}; } `; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js index 074fac9e1d..32606b96d4 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js @@ -1,63 +1,207 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import StyledWrapper from './StyledWrapper'; +import { + IconChevronDown, + IconChevronRight, + IconCircleCheck, + IconCircleX +} from '@tabler/icons'; -const TestResults = ({ results, assertionResults }) => { +const TestResults = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => { results = results || []; assertionResults = assertionResults || []; - if (!results.length && !assertionResults.length) { - return
No tests found
; - } - + preRequestTestResults = preRequestTestResults || []; + postResponseTestResults = postResponseTestResults || []; + const passedTests = results.filter((result) => result.status === 'pass'); const failedTests = results.filter((result) => result.status === 'fail'); const passedAssertions = assertionResults.filter((result) => result.status === 'pass'); const failedAssertions = assertionResults.filter((result) => result.status === 'fail'); + const passedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'pass'); + const failedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail'); + + const passedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'pass'); + const failedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail'); + + const [expandedSections, setExpandedSections] = useState({ + preRequest: true, + tests: true, + postResponse: true, + assertions: true + }); + + // Update expanded sections when test results change + useEffect(() => { + setExpandedSections({ + preRequest: preRequestTestResults.length > 0, + tests: results.length > 0, + postResponse: postResponseTestResults.length > 0, + assertions: assertionResults.length > 0 + }); + }, [results.length, assertionResults.length, preRequestTestResults.length, postResponseTestResults.length]); + + const toggleSection = (section) => { + setExpandedSections({ + ...expandedSections, + [section]: !expandedSections[section] + }); + }; + + if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) { + return
No tests found
; + } + return ( -
- Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length} -
- + {preRequestTestResults.length > 0 && ( +
+
toggleSection('preRequest')} + > + + {expandedSections.preRequest ? + : + + } + + + Pre-Request Tests ({preRequestTestResults.length}), Passed: {passedPreRequestTests.length}, Failed: {failedPreRequestTests.length} + +
+ {expandedSections.preRequest && ( + + )} +
+ )} + + {postResponseTestResults.length > 0 && ( +
+
toggleSection('postResponse')} + > + + {expandedSections.postResponse ? + : + + } + + + Post-Response Tests ({postResponseTestResults.length}), Passed: {passedPostResponseTests.length}, Failed: {failedPostResponseTests.length} + +
+ {expandedSections.postResponse && ( + + )} +
+ )} + + {results.length > 0 && ( +
+
toggleSection('tests')} + > + + {expandedSections.tests ? + : + + } + + + Tests ({results.length}), Passed: {passedTests.length}, Failed: {failedTests.length} + +
+ {expandedSections.tests && ( + + )} +
+ )} -
- Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '} - {failedAssertions.length} -
- + {assertionResults.length > 0 && ( +
+
toggleSection('assertions')} + > + + {expandedSections.assertions ? + : + + } + + + Assertions ({assertionResults.length}), Passed: {passedAssertions.length}, Failed: {failedAssertions.length} + +
+ {expandedSections.assertions && ( + + )} +
+ )}
); }; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js index f894d1f767..1384d06ed4 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js @@ -1,9 +1,13 @@ import React from 'react'; +import { IconCircleCheck, IconCircleX } from '@tabler/icons'; -const TestResultsLabel = ({ results, assertionResults }) => { +const TestResultsLabel = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => { results = results || []; assertionResults = assertionResults || []; - if (!results.length && !assertionResults.length) { + preRequestTestResults = preRequestTestResults || []; + postResponseTestResults = postResponseTestResults || []; + + if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) { return 'Tests'; } @@ -13,8 +17,14 @@ const TestResultsLabel = ({ results, assertionResults }) => { const numberOfAssertions = assertionResults.length; const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length; - const totalNumberOfTests = numberOfTests + numberOfAssertions; - const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions; + const numberOfPreRequestTests = preRequestTestResults.length; + const numberOfFailedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail').length; + + const numberOfPostResponseTests = postResponseTestResults.length; + const numberOfFailedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail').length; + + const totalNumberOfTests = numberOfTests + numberOfAssertions + numberOfPreRequestTests + numberOfPostResponseTests; + const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions + numberOfFailedPreRequestTests + numberOfFailedPostResponseTests; return (
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index ebacf05c55..c118fb1c4f 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -72,7 +72,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { return ; } case 'tests': { - return ; + return ; } default: { @@ -138,7 +143,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { Timeline
selectTab('tests')}> - +
{!isLoading ? (
diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index 5591dbfea3..a5f9c2f636 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -16,7 +16,7 @@ import RunnerTimeline from 'components/ResponsePane/RunnerTimeline'; const ResponsePane = ({ rightPaneWidth, item, collection }) => { const [selectedTab, setSelectedTab] = useState('response'); - const { requestSent, responseReceived, testResults, assertionResults, error } = item; + const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item; const headers = get(item, 'responseReceived.headers', []); const status = get(item, 'responseReceived.status', 0); @@ -49,7 +49,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { return ; } case 'tests': { - return ; + return ; } default: { @@ -86,7 +91,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { Timeline
selectTab('tests')}> - +
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 5e8275ba14..1663eee0c4 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1971,6 +1971,16 @@ export const collectionsSlice = createSlice({ const { results } = action.payload; item.testResults = results; } + + if (type === 'test-results-pre-request') { + const { results } = action.payload; + item.preRequestTestResults = results; + } + + if (type === 'test-results-post-response') { + const { results } = action.payload; + item.postResponseTestResults = results; + } } } }, diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 7745876148..1aab606a76 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -47,6 +47,8 @@ const runSingleRequest = async function ( let request; let nextRequestName; let shouldStopRunnerExecution = false; + let preRequestTestResults = []; + let postResponseTestResults = []; let item = { pathname: path.join(collectionPath, filename), ...bruJson @@ -102,9 +104,28 @@ const runSingleRequest = async function ( skipped: true, assertionResults: [], testResults: [], + preRequestTestResults: [], + postResponseTestResults: [], shouldStopRunnerExecution }; } + + preRequestTestResults = result?.results || []; + + // Display pre-request test results + if (preRequestTestResults?.length) { + console.log(chalk.dim('Pre-Request Tests:')); + each(preRequestTestResults, (r) => { + if (r.status === 'pass') { + console.log(chalk.green(` ✓ `) + chalk.dim(r.description)); + } else { + console.log(chalk.red(` ✕ `) + chalk.red(r.description)); + if (r.error) { + console.log(chalk.red(` ${r.error}`)); + } + } + }); + } } // interpolate variables inside request @@ -434,6 +455,23 @@ const runSingleRequest = async function ( if (result?.stopExecution) { shouldStopRunnerExecution = true; } + + postResponseTestResults = result?.results || []; + + // Display post-response test results + if (postResponseTestResults?.length) { + console.log(chalk.dim('Post-Response Tests:')); + each(postResponseTestResults, (r) => { + if (r.status === 'pass') { + console.log(chalk.green(` ✓ `) + chalk.dim(r.description)); + } else { + console.log(chalk.red(` ✕ `) + chalk.red(r.description)); + if (r.error) { + console.log(chalk.red(` ${r.error}`)); + } + } + }); + } } // run assertions @@ -518,6 +556,8 @@ const runSingleRequest = async function ( error: null, assertionResults, testResults, + preRequestTestResults, + postResponseTestResults, nextRequestName: nextRequestName, shouldStopRunnerExecution }; @@ -542,7 +582,9 @@ const runSingleRequest = async function ( }, error: err.message, assertionResults: [], - testResults: [] + testResults: [], + preRequestTestResults: [], + postResponseTestResults: [] }; } }; diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 31b2f39296..0681edad69 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -253,6 +253,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] each(item, (i) => { + console.log('item i', i); if (isItemAFolder(i)) { const baseFolderName = i.name || 'Untitled Folder'; let folderName = baseFolderName; @@ -379,19 +380,19 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { } } if (event.listen === 'test' && event.script && event.script.exec) { - if (!brunoRequestItem.request.tests) { - brunoRequestItem.request.tests = {}; + if (!brunoRequestItem.request.script) { + brunoRequestItem.request.script = {}; } if (Array.isArray(event.script.exec)) { if (event.script.exec.length > 0) { - brunoRequestItem.request.tests = event.script.exec + brunoRequestItem.request.script.res = event.script.exec .map((line) => postmanTranslation(line)) .join('\n'); } else { - brunoRequestItem.request.tests = ''; + brunoRequestItem.request.script.res = ''; } } else if (typeof event.script.exec === 'string') { - brunoRequestItem.request.tests = postmanTranslation(event.script.exec); + brunoRequestItem.request.script.res = postmanTranslation(event.script.exec); } else { console.warn('Unexpected event.script.exec type', typeof event.script.exec); } diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8fddd2d98d..486f910ab3 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -520,7 +520,7 @@ const registerNetworkIpc = (mainWindow) => { try { - await runPreRequest( + const preRequestScriptResult = await runPreRequest( request, requestUid, envVars, @@ -533,6 +533,16 @@ const registerNetworkIpc = (mainWindow) => { runRequestByItemPathname ); + if (preRequestScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-pre-request', + results: preRequestScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } + !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'pre-request-script-execution', requestUid, @@ -648,7 +658,7 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); try { - await runPostResponse( + const postResponseScriptResult = await runPostResponse( request, response, requestUid, @@ -661,6 +671,16 @@ const registerNetworkIpc = (mainWindow) => { scriptingConfig, runRequestByItemPathname ); + + if (postResponseScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-post-response', + results: postResponseScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'post-response-script-execution', requestUid, diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2a8d02a87d..3ff899b4ac 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -12,6 +12,8 @@ const { get } = require('lodash'); const Bru = require('../bru'); const BrunoRequest = require('../bruno-request'); const BrunoResponse = require('../bruno-response'); +const Test = require('../test'); +const TestResults = require('../test-results'); const { cleanJson } = require('../utils'); // Inbuilt Library Support @@ -32,6 +34,24 @@ const xml2js = require('xml2js'); const cheerio = require('cheerio'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); +const getResultsSummary = (results) => { + const summary = { + total: results.length, + passed: 0, + failed: 0, + skipped: 0, + }; + + results.forEach((r) => { + const passed = r.status === "pass"; + if (passed) summary.passed += 1; + else if (r.status === "fail") summary.failed += 1; + else summary.skipped += 1; + }); + + return summary; +} + class ScriptRuntime { constructor(props) { this.runtime = props?.runtime || 'vm2'; @@ -55,6 +75,7 @@ class ScriptRuntime { const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; + const assertionResults = request?.assertionResults || []; const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); const req = new BrunoRequest(request); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); @@ -76,9 +97,47 @@ class ScriptRuntime { } } + const __brunoTestResults = new TestResults(); + const test = Test(__brunoTestResults, chai); + + bru.getTestResults = async () => { + let results = await __brunoTestResults.getResults(); + const summary = getResultsSummary(results); + return { + summary, + results: results?.map?.(r => ({ + status: r?.status, + description: r?.description, + expected: r?.expected, + actual: r?.actual, + error: r?.error + })) + }; + } + + bru.getAssertionResults = async () => { + let results = assertionResults; + const summary = getResultsSummary(results); + return { + summary, + results: results?.map?.(r => ({ + status: r?.status, + lhsExpr: r?.lhsExpr, + rhsExpr: r?.rhsExpr, + operator: r?.operator, + rhsOperand: r?.rhsOperand, + error: r?.error + })) + }; + } + const context = { bru, - req + req, + test, + expect: chai.expect, + assert: chai.assert, + __brunoTestResults: __brunoTestResults }; if (onConsoleLog && typeof onConsoleLog === 'function') { @@ -112,6 +171,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -165,6 +225,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -188,6 +249,7 @@ class ScriptRuntime { const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; + const assertionResults = request?.assertionResults || []; const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); const req = new BrunoRequest(request); const res = new BrunoResponse(response); @@ -210,10 +272,48 @@ class ScriptRuntime { } } + const __brunoTestResults = new TestResults(); + const test = Test(__brunoTestResults, chai); + + bru.getTestResults = async () => { + let results = await __brunoTestResults.getResults(); + const summary = getResultsSummary(results); + return { + summary, + results: results?.map?.(r => ({ + status: r?.status, + description: r?.description, + expected: r?.expected, + actual: r?.actual, + error: r?.error + })) + }; + } + + bru.getAssertionResults = async () => { + let results = assertionResults; + const summary = getResultsSummary(results); + return { + summary, + results: results?.map?.(r => ({ + status: r?.status, + lhsExpr: r?.lhsExpr, + rhsExpr: r?.rhsExpr, + operator: r?.operator, + rhsOperand: r?.rhsOperand, + error: r?.error + })) + }; + } + const context = { bru, req, - res + res, + test, + expect: chai.expect, + assert: chai.assert, + __brunoTestResults: __brunoTestResults }; if (onConsoleLog && typeof onConsoleLog === 'function') { @@ -247,6 +347,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -300,6 +401,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution