Fix/runner results enhancement (#7040)

* Mark test script errors as failed in runner (#6261)

* Mark test script errors as failed in runner
    and CLI

* Unify handling of post-response and pre-request script errors in both CLI and Electron

* feat: Enhance error handling in script execution by preserving partial results for pre-request and post-response scripts across CLI and Electron. This ensures that tests passing before an error are still reported.

* Preserving stopExecution in test script error handler

---------

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>

* Enhance error handling for script execution by introducing isScriptError flag in test results (#7029)

* fix: Enhance error handling for script execution by introducing isScriptError flag in test results

Enhance error reporting in script execution by adding isScriptError flag to error responses

fix: Mark pre-request script errors as failures in runner summary

---------

Co-authored-by: Karan Pradhan <78605930+KaranPradhan266@users.noreply.github.com>
Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
This commit is contained in:
Bijin A B
2026-02-04 16:26:31 +05:30
committed by GitHub
5 changed files with 298 additions and 90 deletions

View File

@@ -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');
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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();
}
}