feat: enhance hook functionality with runSingleRequestByPathname

- Introduced runSingleRequestByPathname to allow running requests at the collection level.
- Updated executeCollectionHooks to utilize the new function for improved hook execution.
- Added comprehensive tests for bru.runRequest in various hook scenarios, ensuring correct behavior and response handling.
- Removed redundant code and optimized hook management in collection.js.
This commit is contained in:
sanish-bruno
2026-01-22 20:13:10 +05:30
parent 2c474e8052
commit 3d319855bc
7 changed files with 213 additions and 95 deletions

View File

@@ -622,45 +622,7 @@ const handler = async function (argv) {
console[type](...args);
};
// Helper function to execute collection-level hooks at runtime
const executeCollectionHooks = async (hookEvent, eventData) => {
collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
if (!collectionHooks || !collectionHooks.trim()) {
return;
}
try {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
const result = await hooksRuntime.runHooks({
hooksFile: decomment(collectionHooks),
request: {}, // Placeholder request for collection-level hooks
envVariables: envVars,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: null, // Not available at collection level
collectionName
});
if (result?.hookManager) {
await result.hookManager.call(hookEvent, eventData);
// Dispose HookManager to free VM resources
if (result.hookManager && typeof result.hookManager.dispose === 'function') {
result.hookManager.dispose();
}
}
} catch (error) {
console.error(`Error executing collection-level hooks for ${hookEvent}:`, error);
}
};
// Call onBeforeCollectionRun hook before starting to run requests
await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
// Define runSingleRequestByPathname before executeCollectionHooks so it's available at all hook levels
const runSingleRequestByPathname = async (relativeItemPathname) => {
const ext = FORMAT_CONFIG[collection.format].ext;
return new Promise(async (resolve, reject) => {
@@ -688,6 +650,45 @@ const handler = async function (argv) {
});
};
// Helper function to execute collection-level hooks at runtime
const executeCollectionHooks = async (hookEvent, eventData) => {
collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
if (!collectionHooks || !collectionHooks.trim()) {
return;
}
try {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
const result = await hooksRuntime.runHooks({
hooksFile: decomment(collectionHooks),
request: {}, // Placeholder request for collection-level hooks
envVariables: envVars,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: runSingleRequestByPathname,
collectionName
});
if (result?.hookManager) {
await result.hookManager.call(hookEvent, eventData);
// Dispose HookManager to free VM resources
if (result.hookManager && typeof result.hookManager.dispose === 'function') {
result.hookManager.dispose();
}
}
} catch (error) {
console.error(`Error executing collection-level hooks for ${hookEvent}:`, error);
}
};
// Call onBeforeCollectionRun hook before starting to run requests
await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < requestItems.length) {

View File

@@ -429,60 +429,6 @@ const HOOK_EVENTS = Object.freeze({
RUNNER_AFTER_COLLECTION_RUN: 'runner:afterCollectionRun'
});
/**
* Get or create HookManager for a specific level (collection, folder, or request)
* @param {Map} hookManagersMap - Map storing HookManagers by key
* @param {string} key - Unique identifier (collection:${pathname}, folder:${pathname}, or request uid/pathname)
* @param {string} hooksFile - Hooks file content for this level
* @param {object} options - Options for hook registration
* @param {object} options.request - Request object
* @param {object} options.envVars - Environment variables (or envVariables)
* @param {object} options.runtimeVariables - Runtime variables
* @param {string} options.collectionPath - Collection path
* @param {function} options.onConsoleLog - Console log callback
* @param {object} options.processEnvVars - Process environment variables
* @param {object} options.scriptingConfig - Scripting configuration
* @param {function} options.runRequestByItemPathname - Function to run requests
* @param {string} options.collectionName - Collection name
* @returns {Promise<HookManager>} HookManager instance for this level
*/
const getOrCreateHookManager = async (hookManagersMap, key, hooksFile, options = {}) => {
// Return existing HookManager if already created
if (hookManagersMap.has(key)) {
return hookManagersMap.get(key);
}
// Create new HookManager and register hooks
const hookManager = new HookManager();
hookManagersMap.set(key, hookManager);
if (hooksFile && hooksFile.trim()) {
const hooksRuntime = new HooksRuntime({ runtime: options.scriptingConfig?.runtime });
try {
await hooksRuntime.runHooks({
hooksFile: decomment(hooksFile),
hookManager,
request: options.request || {},
envVariables: options.envVars || options.envVariables || {},
runtimeVariables: options.runtimeVariables || {},
collectionPath: options.collectionPath,
onConsoleLog: options.onConsoleLog,
processEnvVars: options.processEnvVars || {},
scriptingConfig: options.scriptingConfig || {},
runRequestByItemPathname: options.runRequestByItemPathname,
collectionName: options.collectionName
});
} catch (error) {
console.error(`Error registering hooks for ${key}:`, error);
if (options.onConsoleLog) {
options.onConsoleLog('error', [`Error registering hooks for ${key}: ${error.message}`]);
}
}
}
return hookManager;
};
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
let requests = [];
@@ -714,6 +660,5 @@ module.exports = {
getAllRequestsAtFolderRoot,
getCallStack,
extractHooks,
HOOK_EVENTS,
getOrCreateHookManager
HOOK_EVENTS
};

View File

@@ -68,6 +68,32 @@ script:hooks {
console.log('[onBeforeCollectionRun] Setup complete - token and counters initialized');
});
// Test: bru.runRequest in onBeforeCollectionRun hook
bru.hooks.runner.onBeforeCollectionRun(async() => {
console.log('[onBeforeCollectionRun] Testing bru.runRequest in collection-level hook...');
try {
// Call another request using bru.runRequest
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
// Store the response for verification in tests
if (response) {
bru.setVar('collection-run-request-response', response.data);
bru.setVar('collection-run-request-status', response.status);
bru.setVar('collection-run-request-tested', 'true');
console.log('[onBeforeCollectionRun] bru.runRequest succeeded:', response.data);
} else {
bru.setVar('collection-run-request-error', 'No response received');
console.log('[onBeforeCollectionRun] bru.runRequest failed - no response');
}
} catch (error) {
bru.setVar('collection-run-request-error', error.message || String(error));
console.log('[onBeforeCollectionRun] bru.runRequest error:', error);
}
console.log('[onBeforeCollectionRun] bru.runRequest test completed');
});
// ============================================
// onAfterCollectionRun Hook Tests
// ============================================
@@ -136,6 +162,10 @@ script:hooks {
bru.deleteVar('success-count');
bru.deleteVar('final-stats');
bru.deleteVar('cleanup-performed');
bru.deleteVar('collection-run-request-response');
bru.deleteVar('collection-run-request-status');
bru.deleteVar('collection-run-request-tested');
bru.deleteVar('collection-run-request-error');
await bru.sleep(300);
console.log('[onAfterCollectionRun] Cleanup complete - all test variables removed');

View File

@@ -0,0 +1,17 @@
meta {
name: helper-request
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
tests {
test("helper request returns pong", function() {
expect(res.getBody()).to.equal('pong');
});
}

View File

@@ -0,0 +1,49 @@
meta {
name: run-request-in-after-hook
type: http
seq: 3
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:hooks {
bru.hooks.http.onAfterResponse(async ({ res }) => {
console.log('[afterResponse] Testing bru.runRequest API');
console.log('[afterResponse] Main request status:', res.getStatus());
// Call another request using bru.runRequest
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
// Store the response body for verification in tests
if (response) {
bru.setVar('after-helper-response-body', response.data);
bru.setVar('after-helper-response-status', response.status);
console.log('[afterResponse] Helper request response:', response.data);
} else {
bru.setVar('after-helper-response-error', 'No response received');
console.log('[afterResponse] Helper request failed - no response');
}
console.log('[afterResponse] bru.runRequest test completed');
});
}
tests {
test("bru.runRequest in afterResponse hook should return helper response", function() {
const helperBody = bru.getVar('after-helper-response-body');
expect(helperBody).to.equal('pong');
});
test("bru.runRequest in afterResponse hook should return status 200", function() {
const helperStatus = bru.getVar('after-helper-response-status');
expect(helperStatus).to.equal(200);
});
test("main request should succeed", function() {
expect(res.getBody()).to.equal('pong');
});
}

View File

@@ -0,0 +1,48 @@
meta {
name: run-request-in-before-hook
type: http
seq: 2
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:hooks {
bru.hooks.http.onBeforeRequest(async ({ req }) => {
console.log('[beforeRequest] Testing bru.runRequest API');
// Call another request using bru.runRequest
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
// Store the response body for verification in tests
if (response) {
bru.setVar('helper-response-body', response.data);
bru.setVar('helper-response-status', response.status);
console.log('[beforeRequest] Helper request response:', response.data);
} else {
bru.setVar('helper-response-error', 'No response received');
console.log('[beforeRequest] Helper request failed - no response');
}
console.log('[beforeRequest] bru.runRequest test completed');
});
}
tests {
test("bru.runRequest in beforeRequest hook should return helper response", function() {
const helperBody = bru.getVar('helper-response-body');
expect(helperBody).to.equal('pong');
});
test("bru.runRequest in beforeRequest hook should return status 200", function() {
const helperStatus = bru.getVar('helper-response-status');
expect(helperStatus).to.equal(200);
});
test("main request should still succeed", function() {
expect(res.getBody()).to.equal('pong');
});
}

View File

@@ -0,0 +1,28 @@
meta {
name: verify-collection-run-request
type: http
seq: 4
}
get {
url: {{host}}/ping
body: none
auth: none
}
tests {
test("collection-level bru.runRequest should have set response body", function() {
const responseBody = bru.getVar('collection-run-request-response');
expect(responseBody).to.equal('pong');
});
test("collection-level bru.runRequest should have set response status", function() {
const responseStatus = bru.getVar('collection-run-request-status');
expect(responseStatus).to.equal(200);
});
test("collection-level bru.runRequest flag should be set", function() {
const flag = bru.getVar('collection-run-request-tested');
expect(flag).to.equal('true');
});
}