diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 378bd4945..29f19155e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -206,27 +206,75 @@ const runSingleRequest = async function ( const collectionName = collection?.brunoConfig?.name; if (requestScriptFile?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); - const result = await scriptRuntime.runRequestScript( - decomment(requestScriptFile), - request, - envVariables, - runtimeVariables, - collectionPath, - onConsoleLog, - processEnvVars, - scriptingConfig, - runSingleRequestByPathname, - collectionName - ); - if (result?.nextRequestName !== undefined) { - nextRequestName = result.nextRequestName; - } + try { + const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile), + request, + envVariables, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runSingleRequestByPathname, + collectionName); + if (result?.nextRequestName !== undefined) { + nextRequestName = result.nextRequestName; + } - if (result?.stopExecution) { - shouldStopRunnerExecution = true; - } + if (result?.stopExecution) { + shouldStopRunnerExecution = true; + } - if (result?.skipRequest) { + if (result?.skipRequest) { + return { + test: { + filename: relativeItemPathname + }, + request: { + method: request.method, + url: request.url, + headers: request.headers, + data: request.data + }, + response: { + status: 'skipped', + statusText: 'request skipped via pre-request script', + data: null, + responseTime: 0 + }, + error: null, + status: 'skipped', + skipped: true, + assertionResults: [], + testResults: [], + preRequestTestResults: result?.results || [], + postResponseTestResults: [], + shouldStopRunnerExecution + }; + } + + preRequestTestResults = result?.results || []; + } catch (error) { + // Pre-request errors are treated as request errors (we return early with status: 'error'), not as failures. Unlike post-response and test script errors, we do not add a synthetic fail and continue. + console.error('Pre-request script execution error:', error); + console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${error.message})`)); + + // Extract partial results from the error (tests that passed before the error) + preRequestTestResults = error?.partialResults?.results || []; + + // Preserve nextRequestName if it was set before the error + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + // Preserve stopExecution if it was set before the error + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(preRequestTestResults, 'Pre-Request Tests'); + + // Pre-request script error: execution didn't complete (request never sent). Return early so we don't run the HTTP request, post-response script, assertions, or tests. return { test: { filename: relativeItemPathname @@ -238,23 +286,23 @@ const runSingleRequest = async function ( data: request.data }, response: { - status: 'skipped', - statusText: 'request skipped via pre-request script', + status: 'error', + statusText: null, + headers: null, data: null, + url: null, responseTime: 0 }, - error: null, - status: 'skipped', - skipped: true, + error: error?.message || 'An error occurred while executing the pre-request script.', + status: 'error', assertionResults: [], testResults: [], - preRequestTestResults: result?.results || [], + preRequestTestResults, postResponseTestResults: [], + nextRequestName: nextRequestName, shouldStopRunnerExecution }; } - - preRequestTestResults = result?.results || []; } // interpolate variables inside request @@ -732,6 +780,27 @@ const runSingleRequest = async function ( logResults(postResponseTestResults, 'Post-Response Tests'); } catch (error) { console.error('Post-response script execution error:', error); + + const partialResults = error?.partialResults?.results || []; + postResponseTestResults = [ + ...partialResults, + { + status: 'fail', + description: 'Post-Response Script Error', + error: error.message || 'An error occurred while executing the post-response script.', + isScriptError: true + } + ]; + + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(postResponseTestResults, 'Post-Response Tests'); } } @@ -781,6 +850,27 @@ const runSingleRequest = async function ( logResults(testResults, 'Tests'); } catch (error) { console.error('Test script execution error:', error); + + const partialResults = error?.partialResults?.results || []; + testResults = [ + ...partialResults, + { + status: 'fail', + description: 'Test Script Error', + error: error.message || 'An error occurred while executing the test script.', + isScriptError: true + } + ]; + + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(testResults, 'Tests'); } } diff --git a/packages/bruno-common/src/runner/runner-summary.ts b/packages/bruno-common/src/runner/runner-summary.ts index 3e99e676f..bde8f8efb 100644 --- a/packages/bruno-common/src/runner/runner-summary.ts +++ b/packages/bruno-common/src/runner/runner-summary.ts @@ -23,10 +23,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R for (const result of results || []) { const { status, testResults, assertionResults, preRequestTestResults, postResponseTestResults } = result; totalRequests += 1; - totalTests += Number(testResults?.length) || 0; + totalTests += Number(testResults?.filter((r) => !r.isScriptError).length) || 0; totalAssertions += Number(assertionResults?.length) || 0; - totalPreRequestTests += Number(preRequestTestResults?.length) || 0; - totalPostResponseTests += Number(postResponseTestResults?.length) || 0; + totalPreRequestTests += Number(preRequestTestResults?.filter((r) => !r.isScriptError).length) || 0; + totalPostResponseTests += Number(postResponseTestResults?.filter((r) => !r.isScriptError).length) || 0; if (status === 'skipped') { skippedRequests += 1; @@ -35,6 +35,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R let anyFailed = false; for (const testResult of testResults || []) { + if (testResult.isScriptError) { + anyFailed = true; + continue; + } if (testResult.status === 'pass') { passedTests += 1; } else { @@ -51,6 +55,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R } } for (const preRequestTestResult of preRequestTestResults || []) { + if (preRequestTestResult.isScriptError) { + anyFailed = true; + continue; + } if (preRequestTestResult.status === 'pass') { passedPreRequestTests += 1; } else { @@ -59,6 +67,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R } } for (const postResponseTestResult of postResponseTestResults || []) { + if (postResponseTestResult.isScriptError) { + anyFailed = true; + continue; + } if (postResponseTestResult.status === 'pass') { passedPostResponseTests += 1; } else { diff --git a/packages/bruno-common/src/runner/types/index.ts b/packages/bruno-common/src/runner/types/index.ts index 1b696ba12..0d9b40eb5 100644 --- a/packages/bruno-common/src/runner/types/index.ts +++ b/packages/bruno-common/src/runner/types/index.ts @@ -23,6 +23,7 @@ type T_TestPassResult = { status: string; description: string; uid?: string; + isScriptError?: boolean; }; type T_TestFailResult = { @@ -30,6 +31,7 @@ type T_TestFailResult = { description: string; error: string; uid?: string; + isScriptError?: boolean; }; type T_TestResult = T_TestPassResult | T_TestFailResult; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index ab5d8d3a9..0fdf02ecc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -443,6 +443,39 @@ const registerNetworkIpc = (mainWindow) => { }); }; + const appendScriptErrorResult = (scriptType, scriptResult, error) => { + if (!error) { + return scriptResult; + } + + const descriptionMap = { + 'test': 'Test Script Error', + 'post-response': 'Post-Response Script Error', + 'pre-request': 'Pre-Request Script Error' + }; + + const messageMap = { + 'test': 'An error occurred while executing the test script.', + 'post-response': 'An error occurred while executing the post-response script.', + 'pre-request': 'An error occurred while executing the pre-request script.' + }; + + const results = [ + ...(scriptResult?.results || []), + { + status: 'fail', + description: descriptionMap[scriptType] || 'Script Error', + error: error.message || messageMap[scriptType] || 'An error occurred while executing the script.', + isScriptError: true + } + ]; + + return { + ...(scriptResult || {}), + results + }; + }; + const runPreRequest = async ( request, requestUid, @@ -713,6 +746,12 @@ const registerNetworkIpc = (mainWindow) => { preRequestError = error; } + if (preRequestError?.partialResults) { + preRequestScriptResult = preRequestError.partialResults; + } + + preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError); + if (preRequestScriptResult?.results) { mainWindow.webContents.send('main:run-request-event', { type: 'test-results-pre-request', @@ -878,6 +917,15 @@ const registerNetworkIpc = (mainWindow) => { postResponseError = error; } + // Extract partial results from error if available + // This preserves any test() calls that passed before the script errored + // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests) + if (postResponseError?.partialResults) { + postResponseScriptResult = postResponseError.partialResults; + } + + postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError); + if (postResponseScriptResult?.results) { mainWindow.webContents.send('main:run-request-event', { type: 'test-results-post-response', @@ -951,6 +999,8 @@ const registerNetworkIpc = (mainWindow) => { } } + testResults = appendScriptErrorResult('test', testResults, testError); + !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'test-results', results: testResults.results, @@ -1336,6 +1386,12 @@ const registerNetworkIpc = (mainWindow) => { preRequestError = error; } + if (preRequestError?.partialResults) { + preRequestScriptResult = preRequestError.partialResults; + } + + preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError); + if (preRequestScriptResult?.results) { mainWindow.webContents.send('main:run-folder-event', { type: 'test-results-pre-request', @@ -1558,6 +1614,14 @@ const registerNetworkIpc = (mainWindow) => { postResponseError = error; } + // Extract partial results from error if available + // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests) + if (postResponseError?.partialResults) { + postResponseScriptResult = postResponseError.partialResults; + } + + postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError); + notifyScriptExecution({ channel: 'main:run-folder-event', basePayload: eventData, @@ -1644,6 +1708,8 @@ const registerNetworkIpc = (mainWindow) => { } } + testResults = appendScriptErrorResult('test', testResults, testError); + if (testResults?.nextRequestName !== undefined) { nextRequestName = testResults.nextRequestName; } diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index d41b51284..28cd48e75 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -68,35 +68,9 @@ class ScriptRuntime { context.bru.runRequest = runRequestByItemPathname; } - if (this.runtime === 'nodevm') { - await runScriptInNodeVm({ - script, - context, - collectionPath, - scriptingConfig - }); - - return { - request, - envVariables: cleanJson(envVariables), - runtimeVariables: cleanJson(runtimeVariables), - persistentEnvVariables: bru.persistentEnvVariables, - globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), - results: cleanJson(__brunoTestResults.getResults()), - nextRequestName: bru.nextRequest, - skipRequest: bru.skipRequest, - stopExecution: bru.stopExecution - }; - } - - // default runtime is `quickjs` - await executeQuickJsVmAsync({ - script: script, - context: context, - collectionPath - }); - - return { + // Helper to build the result object for pre-request scripts + // Extracted to avoid duplication across runtime branches + const buildRequestScriptResult = () => ({ request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), @@ -106,7 +80,52 @@ class ScriptRuntime { nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution - }; + }); + + // Track script errors to attach partial results before re-throwing + // This ensures that any test() calls that passed before the error are preserved + // Similar pattern to test-runtime.js which already handles this correctly + let scriptError = null; + + if (this.runtime === 'nodevm') { + try { + await runScriptInNodeVm({ + script, + context, + collectionPath, + scriptingConfig + }); + } catch (error) { + scriptError = error; + } + + // If script errored, attach partial results so callers can display passed tests + // before the error occurred (e.g., 2 tests pass, then script throws) + if (scriptError) { + scriptError.partialResults = buildRequestScriptResult(); + throw scriptError; + } + + return buildRequestScriptResult(); + } + + // default runtime is `quickjs` + try { + await executeQuickJsVmAsync({ + script: script, + context: context, + collectionPath + }); + } catch (error) { + scriptError = error; + } + + if (scriptError) { + scriptError.partialResults = buildRequestScriptResult(); + throw scriptError; + } + + return buildRequestScriptResult(); } async runResponseScript( @@ -166,35 +185,9 @@ class ScriptRuntime { context.bru.runRequest = runRequestByItemPathname; } - if (this.runtime === 'nodevm') { - await runScriptInNodeVm({ - script, - context, - collectionPath, - scriptingConfig - }); - - return { - response, - envVariables: cleanJson(envVariables), - persistentEnvVariables: cleanJson(bru.persistentEnvVariables), - runtimeVariables: cleanJson(runtimeVariables), - globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), - results: cleanJson(__brunoTestResults.getResults()), - nextRequestName: bru.nextRequest, - skipRequest: bru.skipRequest, - stopExecution: bru.stopExecution - }; - } - - // default runtime is `quickjs` - await executeQuickJsVmAsync({ - script: script, - context: context, - collectionPath - }); - - return { + // Helper to build the result object for post-response scripts + // Extracted to avoid duplication across runtime branches + const buildResponseScriptResult = () => ({ response, envVariables: cleanJson(envVariables), persistentEnvVariables: cleanJson(bru.persistentEnvVariables), @@ -204,7 +197,52 @@ class ScriptRuntime { nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution - }; + }); + + // Track script errors to attach partial results before re-throwing + // This ensures that any test() calls that passed before the error are preserved + // Similar pattern to test-runtime.js which already handles this correctly + let scriptError = null; + + if (this.runtime === 'nodevm') { + try { + await runScriptInNodeVm({ + script, + context, + collectionPath, + scriptingConfig + }); + } catch (error) { + scriptError = error; + } + + // If script errored, attach partial results so callers can display passed tests + // before the error occurred (e.g., 2 tests pass, then script throws) + if (scriptError) { + scriptError.partialResults = buildResponseScriptResult(); + throw scriptError; + } + + return buildResponseScriptResult(); + } + + // default runtime is `quickjs` + try { + await executeQuickJsVmAsync({ + script: script, + context: context, + collectionPath + }); + } catch (error) { + scriptError = error; + } + + if (scriptError) { + scriptError.partialResults = buildResponseScriptResult(); + throw scriptError; + } + + return buildResponseScriptResult(); } }