diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index bcfd07f24..2c0fb3dd1 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -5,7 +5,7 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import HttpMethodSelector from './HttpMethodSelector'; import { useTheme } from 'providers/Theme'; -import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons'; +import { IconDeviceFloppy, IconArrowRight, IconCode, IconX } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; import { isMacOS } from 'utils/common/platform'; import { hasRequestChanges } from 'utils/collections'; @@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
gRPC
- + ) : ( )} @@ -149,7 +149,13 @@ const QueryUrl = ({ item, collection, handleRun }) => { Save ({saveShortcut}) - + { + item.response?.hasStreamRunning ? ( + + ) : ( + + ) + } {generateCodeItemModalOpen && ( diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index f7880e509..a347c1d0a 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -10,7 +10,7 @@ import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane'; import Welcome from 'components/Welcome'; import { findItemInCollection } from 'utils/collections'; import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs'; -import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import RequestNotFound from './RequestNotFound'; import QueryUrl from 'components/RequestPane/QueryUrl/index'; import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index'; @@ -74,8 +74,7 @@ const RequestTabPanel = () => { const screenWidth = useSelector((state) => state.app.screenWidth); let asideWidth = useSelector((state) => state.app.leftSidebarWidth); const [leftPaneWidth, setLeftPaneWidth] = useState( - focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2 - ); // 2.2 is intentional to make both panes appear to be of equal width + focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 is intentional to make both panes appear to be of equal width const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT); const [dragging, setDragging] = useState(false); const dragOffset = useRef({ x: 0, y: 0 }); @@ -141,12 +140,10 @@ const RequestTabPanel = () => { setDragging(false); if (!isVerticalLayout) { const mainRect = mainSectionRef.current.getBoundingClientRect(); - dispatch( - updateRequestPaneTabWidth({ - uid: activeTabUid, - requestPaneWidth: e.clientX - mainRect.left - }) - ); + dispatch(updateRequestPaneTabWidth({ + uid: activeTabUid, + requestPaneWidth: e.clientX - mainRect.left + })); } } }; @@ -263,11 +260,17 @@ const RequestTabPanel = () => { return; } - dispatch(sendRequest(item, collection.uid)).catch((err) => - toast.custom((t) => toast.dismiss(t.id)} />, { - duration: 5000 - }) - ); + if (item.response?.hasStreamRunning) { + dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) => + toast.custom((t) => toast.dismiss(t.id)} />, { + duration: 5000 + })); + } else if (item.requestState !== 'sending' && item.requestState !== 'queued') { + dispatch(sendRequest(item, collection.uid)).catch((err) => + toast.custom((t) => toast.dismiss(t.id)} />, { + duration: 5000 + })); + } }; // TODO: reaper, improve selection of panes diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js new file mode 100644 index 000000000..c2fe19cfb --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-size: 0.75rem; + font-weight: 600; + color: ${(props) => props.theme.requestTabPanel.responseStatus}; + text-align: center; +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js new file mode 100644 index 000000000..ef96bc891 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js @@ -0,0 +1,27 @@ +import React, { useState, useEffect } from 'react'; +import StyledWrapper from './StyledWrapper'; + +const ResponseStopWatch = ({ startMillis }) => { + const [milliseconds, setMilliseconds] = useState(startMillis); + + const tickInterval = 100; + const tick = () => { + setMilliseconds(_milliseconds => _milliseconds + tickInterval); + }; + + useEffect(() => { + let timerID = setInterval(() => { + tick() + }, tickInterval); + return () => { + clearTimeout(timerID); + }; + }, []); + + let seconds = milliseconds / 1000; + let secondsFormatted = `${seconds.toFixed(1)}s`; + let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast + return {secondsFormatted}; +}; + +export default React.memo(ResponseStopWatch); diff --git a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js index 222aad9e8..302b9a5e2 100644 --- a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js +++ b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js @@ -4,7 +4,7 @@ import statusCodePhraseMap from './get-status-code-phrase'; import StyledWrapper from './StyledWrapper'; // Todo: text-error class is not getting pulled in for 500 errors -const StatusCode = ({ status, statusText }) => { +const StatusCode = ({ status, statusText, isStreaming }) => { const getTabClassname = (status) => { return classnames('ml-2', { 'text-ok': status >= 100 && status < 200, @@ -17,7 +17,7 @@ const StatusCode = ({ status, statusText }) => { return ( - {status} {statusText || statusCodePhraseMap[status]} + {status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null} ); }; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 6feb98bdb..2f173280d 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -21,6 +21,8 @@ import ResponseClear from 'src/components/ResponsePane/ResponseClear'; import ResponseBookmark from 'src/components/ResponsePane/ResponseBookmark'; import SkippedRequest from './SkippedRequest'; import ClearTimeline from './ClearTimeline/index'; +import StopWatch from 'components/StopWatch'; +import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch'; import ResponseLayoutToggle from './ResponseLayoutToggle'; import HeightBoundContainer from 'ui/HeightBoundContainer'; @@ -87,15 +89,17 @@ const ResponsePane = ({ item, collection }) => { return ; } case 'timeline': { - return ; + return ; } case 'tests': { - return ; + return ( + + ); } default: { @@ -184,7 +188,10 @@ const ResponsePane = ({ item, collection }) => { - + + {item.response?.hasStreamRunning ? ( + + ) : } @@ -193,7 +200,7 @@ const ResponsePane = ({ item, collection }) => { ) : null}
{ onClose={() => setShowScriptErrorCard(false)} /> )} -
+
{!item?.response ? ( - focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? ( + focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? ( { dispatch(processEnvUpdateEvent(val)); }); - const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { - console[val.type](...val.args); + const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { + console[val.type](...val.args); dispatch(addLog({ type: val.type, args: val.args, @@ -188,6 +190,14 @@ const useIpcEvents = () => { dispatch(collectionAddOauth2CredentialsByUrl(payload)); }); + const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => { + dispatch(streamDataReceived(val)); + }); + + const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => { + dispatch(requestCancelled(val)); + }); + const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => { dispatch(updateCollectionLoadingState(val)); }); @@ -212,6 +222,8 @@ const useIpcEvents = () => { removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); removeCollectionOauth2CredentialsUpdatesListener(); + removeHttpStreamNewDataListener(); + removeHttpStreamEndListener(); removeCollectionLoadingStateListener(); removePersistentEnvVariablesUpdateListener(); removeSystemResourcesListener(); 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 e6c7ae880..eafba26bd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -83,8 +83,8 @@ const initiatedGrpcResponse = { isError: false, duration: 0, responses: [], - timestamp: Date.now(), -} + timestamp: Date.now() +}; const initiatedWsResponse = { status: 'PENDING', @@ -380,7 +380,15 @@ export const collectionsSlice = createSlice({ if (collection) { const item = findItemInCollection(collection, itemUid); if (item) { - item.response = null; + if (item.response?.hasStreamRunning) { + item.response.hasStreamRunning = null; + + const startTimestamp = item.requestSent.timestamp; + item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration; + } else { + item.response = null; + } + item.cancelTokenUid = null; item.requestUid = null; item.requestStartTime = null; @@ -389,22 +397,22 @@ export const collectionsSlice = createSlice({ }, responseReceived: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); - + if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); if (item) { item.requestState = 'received'; item.response = action.payload.response; - item.cancelTokenUid = null; + item.cancelTokenUid = item.response.hasStreamRunning ? item.cancelTokenUid : null; item.requestStartTime = null; if (!collection.timeline) { collection.timeline = []; } - + // Ensure timestamp is a number (milliseconds since epoch) - const timestamp = item?.requestSent?.timestamp instanceof Date - ? item.requestSent.timestamp.getTime() + const timestamp = item?.requestSent?.timestamp instanceof Date + ? item.requestSent.timestamp.getTime() : item?.requestSent?.timestamp || Date.now(); // Append the new timeline entry with numeric timestamp @@ -427,7 +435,7 @@ export const collectionsSlice = createSlice({ const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) return; - + const item = findItemInCollection(collection, itemUid); if (!item) return; const request = item.draft ? item.draft.request : item.request; @@ -447,7 +455,7 @@ export const collectionsSlice = createSlice({ } collection.timeline.push({ - type: "request", + type: 'request', eventType: eventType, // Add the specific gRPC event type collectionUid: collection.uid, folderUid: null, @@ -456,36 +464,34 @@ export const collectionsSlice = createSlice({ data: { request: eventData || item.requestSent || item.request, timestamp: Date.now(), - eventData: eventData, + eventData: eventData } }); - }, grpcResponseReceived: (state, action) => { const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - + if (!collection) return; const item = findItemInCollection(collection, itemUid); if (!item) return; - + // Get current response state or create initial state - const currentResponse = item.response || initiatedGrpcResponse + const currentResponse = item.response || initiatedGrpcResponse; const timestamp = item?.requestSent?.timestamp; let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) }; - // Process based on event type switch (eventType) { case 'response': const { error, res } = eventData; - + // Handle error if present if (error) { const errorCode = error.code || 2; // Default to UNKNOWN if no code - + updatedResponse.error = error.details || 'gRPC error occurred'; updatedResponse.statusCode = errorCode; updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN'; @@ -494,72 +500,72 @@ export const collectionsSlice = createSlice({ } // Add response to list - updatedResponse.responses = res - ? [...(currentResponse?.responses || []), res] + updatedResponse.responses = res + ? [...(currentResponse?.responses || []), res] : [...(currentResponse?.responses || [])]; break; - + case 'metadata': updatedResponse.headers = eventData.metadata; updatedResponse.metadata = eventData.metadata; break; - + case 'status': // Extract status info const statusCode = eventData.status?.code; const statusDetails = eventData.status?.details; const statusMetadata = eventData.status?.metadata; - + // Set status based on actual code and details updatedResponse.statusCode = statusCode; updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN'; updatedResponse.statusDescription = statusDetails; updatedResponse.statusDetails = eventData.status; - + // Store trailers (status metadata) if (statusMetadata) { updatedResponse.trailers = statusMetadata; } - + // Handle error status (non-zero code) if (statusCode !== 0) { updatedResponse.isError = true; updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; } - + break; - + case 'error': // Extract error details const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code const errorDetails = eventData.error?.details || eventData.error?.message; const errorMetadata = eventData.error?.metadata; - + updatedResponse.isError = true; updatedResponse.error = errorDetails || 'Unknown gRPC error'; updatedResponse.statusCode = errorCode; updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN'; updatedResponse.statusDescription = errorDetails; - + // Store error metadata as trailers if present if (errorMetadata) { updatedResponse.trailers = errorMetadata; } - + break; - + case 'end': - state.activeConnections = state.activeConnections.filter(id => id !== itemUid); + state.activeConnections = state.activeConnections.filter((id) => id !== itemUid); break; - + case 'cancel': updatedResponse.statusCode = 1; // CANCELLED updatedResponse.statusText = 'CANCELLED'; updatedResponse.statusDescription = 'Stream cancelled by client or server'; - state.activeConnections = state.activeConnections.filter(id => id !== itemUid); + state.activeConnections = state.activeConnections.filter((id) => id !== itemUid); break; } - + item.requestState = 'received'; item.response = updatedResponse; @@ -570,7 +576,7 @@ export const collectionsSlice = createSlice({ // Append the new timeline entry with specific gRPC event type collection.timeline.push({ - type: "request", + type: 'request', eventType: eventType, // Add the specific gRPC event type collectionUid: collection.uid, folderUid: null, @@ -580,7 +586,7 @@ export const collectionsSlice = createSlice({ request: item.requestSent || item.request, response: updatedResponse, eventData: eventData, // Store the original event data - timestamp: Date.now(), + timestamp: Date.now() } }); }, @@ -590,6 +596,12 @@ export const collectionsSlice = createSlice({ if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); if (item) { + if (item.response && item.response.hasStreamRunning) { + item.response.data = ''; + item.response.size = 0; + return; + } + item.response = null; } } @@ -916,7 +928,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || []; + const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || []; const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name, @@ -930,9 +942,7 @@ export const collectionsSlice = createSlice({ // Update the request URL to reflect the new query params const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams( - filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') - ); + const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); // If there are enabled query params, append them to the URL if (query && query.length) { @@ -1163,7 +1173,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({ + item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name: name, value: value, @@ -1205,8 +1215,8 @@ export const collectionsSlice = createSlice({ if (!folder || !isItemAFolder(folder)) { return; } - - folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({ + + folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name: name, value: value, @@ -1487,7 +1497,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - + switch (item.draft.request.body.mode) { case 'json': { item.draft.request.body.json = action.payload.content; @@ -1624,7 +1634,7 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { - const item = findItemInCollection(collection, action.payload.itemUid); + const item = findItemInCollection(collection, action.payload.itemUid); if (item && isItemARequest(item)) { if (!item.draft) { @@ -1875,7 +1885,7 @@ export const collectionsSlice = createSlice({ break; case 'ntlm': set(collection, 'draft.root.request.auth.ntlm', action.payload.content); - break; + break; case 'oauth2': set(collection, 'draft.root.request.auth.oauth2', action.payload.content); break; @@ -2604,7 +2614,7 @@ export const collectionsSlice = createSlice({ const { requestUid, itemUid, collectionUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) return; - + const item = findItemInCollection(collection, itemUid); if (!item) return; @@ -2637,7 +2647,7 @@ export const collectionsSlice = createSlice({ item.postResponseScriptErrorMessage = action.payload.errorMessage; } - if(type === 'test-script-execution') { + if (type === 'test-script-execution') { item.testScriptErrorMessage = action.payload.errorMessage; } @@ -2652,7 +2662,7 @@ export const collectionsSlice = createSlice({ if (type === 'request-sent') { const { cancelTokenUid, requestSent } = action.payload; item.requestSent = requestSent; - + // sometimes the response is received before the request-sent event arrives if (item.requestState === 'queued') { item.requestState = 'sending'; @@ -2669,12 +2679,12 @@ 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; @@ -2788,7 +2798,7 @@ export const collectionsSlice = createSlice({ if (collection) { collection.runnerResult = null; - collection.runnerTags = { include: [], exclude: [] } + collection.runnerTags = { include: [], exclude: [] }; collection.runnerTagsEnabled = false; collection.runnerConfiguration = null; } @@ -2927,7 +2937,7 @@ export const collectionsSlice = createSlice({ updateFolderAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null; - + if (folder) { if (!folder.draft) { folder.draft = cloneDeep(folder.root); @@ -2936,7 +2946,16 @@ export const collectionsSlice = createSlice({ set(folder, 'draft.request.auth.mode', action.payload.mode); } }, + streamDataReceived: (state, action) => { + const { itemUid, collectionUid, data } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (collection) { + const item = findItemInCollection(collection, itemUid); + item.response.data = data.data + (item.response.data || ''); + item.response.size = data.data?.length + (item.response.size || 0); + } + }, addRequestTag: (state, action) => { const { tag, collectionUid, itemUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -2978,7 +2997,7 @@ export const collectionsSlice = createSlice({ updateCollectionTagsList: (state, action) => { const { collectionUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - + if (collection) { collection.allTags = getUniqueTagsFromItems(collection.items); } @@ -3298,6 +3317,7 @@ export const { updateRequestDocs, updateFolderDocs, moveCollection, + streamDataReceived, collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByUrl, collectionGetOauth2CredentialsByUrl, diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index 7c1e3b74a..c788a1a93 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -20,7 +20,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV status: response.status, statusText: response.statusText, duration: response.duration, - timeline: response.timeline + timeline: response.timeline, + hasStreamRunning: response.hasStreamRunning }); }) .catch((err) => reject(err)); @@ -31,19 +32,17 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => { return new Promise((resolve, reject) => { startGrpcRequest(item, collection, environment, runtimeVariables) - .then((initialState) => { - // Return an initial state object to update the UI - // The real response data will be handled by event listeners - resolve({ - ...initialState, - timeline: [] - }); - }) - .catch((err) => reject(err)); + .then((initialState) => { + // Return an initial state object to update the UI + // The real response data will be handled by event listeners + resolve({ + ...initialState, + timeline: [] + }); + }) + .catch((err) => reject(err)); }); -} - - +}; const sendHttpRequest = async (item, collection, environment, runtimeVariables) => { return new Promise((resolve, reject) => { @@ -83,19 +82,19 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar return new Promise((resolve, reject) => { const { ipcRenderer } = window; const request = item.draft ? item.draft : item; - + ipcRenderer.invoke('grpc:start-connection', { - request, - collection, - environment, + request, + collection, + environment, runtimeVariables }) - .then(() => { - resolve(); - }) - .catch(err => { - reject(err); - }); + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); }); }; @@ -188,7 +187,7 @@ export const isGrpcConnectionActive = async (connectionId) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; ipcRenderer.invoke('grpc:is-connection-active', connectionId) - .then(response => { + .then((response) => { if (response.success) { resolve(response.isActive); } else { @@ -197,7 +196,7 @@ export const isGrpcConnectionActive = async (connectionId) => { resolve(false); } }) - .catch(err => { + .catch((err) => { console.error('Failed to check connection status:', err); // On error, assume the connection is not active resolve(false); @@ -215,14 +214,14 @@ export const isGrpcConnectionActive = async (connectionId) => { export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; - - ipcRenderer.invoke('grpc:generate-sample-message', { - methodPath, - existingMessage, - options + + ipcRenderer.invoke('grpc:generate-sample-message', { + methodPath, + existingMessage, + options }) - .then(resolve) - .catch(reject); + .then(resolve) + .catch(reject); }); }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 611182a27..14956c035 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -69,16 +69,44 @@ const getJsSandboxRuntime = (collection) => { return 'vm2'; }; -const configureRequest = async ( - collectionUid, +const isStream = (headers) => { + return headers.get('content-type') === 'text/event-stream'; +}; + +const promisifyStream = async (stream, abortController, closeOnFirst) => { + const chunks = []; + + return new Promise((resolve, reject) => { + const doResolve = () => { + const fullBuffer = Buffer.concat(chunks); + resolve(fullBuffer.buffer.slice(fullBuffer.byteOffset, fullBuffer.byteOffset + fullBuffer.byteLength)); + }; + + stream.on('data', (chunk) => { + chunks.push(chunk); + + if (closeOnFirst) { + doResolve(); + + if (abortController) { + abortController.abort(); + } + } + }); + + stream.on('close', doResolve); + stream.on('error', err => reject(err)); + }); +}; + +const configureRequest = async (collectionUid, collection, request, envVars, runtimeVariables, processEnvVars, collectionPath, - globalEnvironmentVariables -) => { + globalEnvironmentVariables) => { const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; if (!protocolRegex.test(request.url)) { request.url = `http://${request.url}`; @@ -97,7 +125,7 @@ const configureRequest = async ( // Get followRedirects setting, default to true for backward compatibility const followRedirects = request.settings?.followRedirects ?? true; - + // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5 let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5; @@ -138,14 +166,12 @@ const configureRequest = async ( request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; if (tokenPlacement == 'header' && credentials?.access_token) { request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim(); - } - else { + } else { try { const url = new URL(request.url); url?.searchParams?.set(tokenQueryKey, credentials?.access_token); request.url = url?.toString(); - } - catch(error) {} + } catch (error) {} } break; case 'implicit': @@ -154,8 +180,7 @@ const configureRequest = async ( request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; if (tokenPlacement == 'header') { request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; - } - else { + } else { try { const url = new URL(request.url); url?.searchParams?.set(tokenQueryKey, credentials?.access_token); @@ -217,8 +242,7 @@ const configureRequest = async ( if (preferencesUtil.shouldSendCookies()) { const cookieString = getCookieStringForUrl(request.url); if (cookieString && typeof cookieString === 'string' && cookieString.length) { - const existingCookieHeaderName = Object.keys(request.headers).find( - name => name.toLowerCase() === 'cookie' + const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie' ); const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : ''; @@ -282,8 +306,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars const processEnvVars = getProcessEnvVars(collection.uid); - const resolvedVars = merge( - {}, + const resolvedVars = merge({}, globalEnvironmentVars, collectionVariables, envVars, @@ -296,8 +319,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col ...processEnvVars } } - } - ); + }); const collectionRoot = collection?.draft?.root || collection?.root || {}; const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot); @@ -314,16 +336,14 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const collectionPath = collection.pathname; - const axiosInstance = await configureRequest( - collection.uid, + const axiosInstance = await configureRequest(collection.uid, collection, request, envVars, collection.runtimeVariables, processEnvVars, collectionPath, - collection.globalEnvironmentVariables - ); + collection.globalEnvironmentVariables); const response = await axiosInstance(request); @@ -358,10 +378,10 @@ const registerNetworkIpc = (mainWindow) => { }; const notifyScriptExecution = ({ - channel, // 'main:run-request-event' | 'main:run-folder-event' - basePayload, // request-level or runner-level identifiers - scriptType, // 'pre-request' | 'post-response' | 'test' - error // optional Error + channel, // 'main:run-request-event' | 'main:run-folder-event' + basePayload, // request-level or runner-level identifiers + scriptType, // 'pre-request' | 'post-response' | 'test' + error // optional Error }) => { mainWindow.webContents.send(channel, { type: `${scriptType}-script-execution`, @@ -370,8 +390,7 @@ const registerNetworkIpc = (mainWindow) => { }); }; - const runPreRequest = async ( - request, + const runPreRequest = async (request, requestUid, envVars, collectionPath, @@ -380,11 +399,10 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ) => { + runRequestByItemPathname) => { // run pre-request script let scriptResult; - const collectionName = collection?.name + const collectionName = collection?.name; const requestScript = get(request, 'script.req'); if (requestScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); @@ -462,8 +480,7 @@ const registerNetworkIpc = (mainWindow) => { return scriptResult; }; - const runPostResponse = async ( - request, + const runPostResponse = async (request, response, requestUid, envVars, @@ -520,7 +537,7 @@ const registerNetworkIpc = (mainWindow) => { // run post-response script const responseScript = get(request, 'script.res'); let scriptResult; - const collectionName = collection?.name + const collectionName = collection?.name; if (responseScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); scriptResult = await scriptRuntime.runResponseScript( @@ -595,7 +612,9 @@ const registerNetworkIpc = (mainWindow) => { const abortController = new AbortController(); const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; + request.responseType = "stream"; const brunoConfig = getBrunoConfig(collectionUid, collection); + const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); @@ -606,8 +625,7 @@ const registerNetworkIpc = (mainWindow) => { let preRequestScriptResult = null; let preRequestError = null; try { - preRequestScriptResult = await runPreRequest( - request, + preRequestScriptResult = await runPreRequest(request, requestUid, envVars, collectionPath, @@ -616,8 +634,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { preRequestError = error; } @@ -642,16 +659,14 @@ const registerNetworkIpc = (mainWindow) => { if (preRequestError) { return Promise.reject(preRequestError); } - const axiosInstance = await configureRequest( - collectionUid, + const axiosInstance = await configureRequest(collectionUid, collection, request, envVars, runtimeVariables, processEnvVars, collectionPath, - collection.globalEnvironmentVariables - ); + collection.globalEnvironmentVariables); const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request); @@ -668,8 +683,9 @@ const registerNetworkIpc = (mainWindow) => { method: request.method, headers: headersSent, data: requestData, - dataBuffer: requestDataBuffer - } + dataBuffer: requestDataBuffer, + timestamp: Date.now() + }; !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'request-sent', @@ -695,6 +711,11 @@ const registerNetworkIpc = (mainWindow) => { try { /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); + request.isStream = isStream(response.headers); + + if (!request.isStream) { + response.data = await promisifyStream(response.data); + } // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); @@ -719,6 +740,11 @@ const registerNetworkIpc = (mainWindow) => { // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); + + request.isStream = isStream(response.headers); + if (!request.isStream) { + response.data = await promisifyStream(response.data); + } } else { await executeRequestOnFailHandler(request, error); @@ -729,16 +755,21 @@ const registerNetworkIpc = (mainWindow) => { statusText: error.statusText, error: error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST, timeline: error.timeline - } + }; } } // Continue with the rest of the request lifecycle - post response vars, script, assertions, tests - const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); - response.data = data; - response.dataBuffer = dataBuffer; + if (request.isStream) { + response.stream = response.data; + } + const { data, dataBuffer } = request.isStream + ? { data: '', dataBuffer: new ArrayBuffer(0) } + : parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); + + response.data = data; response.responseTime = responseTime; // save cookies @@ -754,138 +785,140 @@ const registerNetworkIpc = (mainWindow) => { let postResponseScriptResult = null; let postResponseError = null; - try { - postResponseScriptResult = await runPostResponse( - request, - response, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig, - runRequestByItemPathname - ); - } catch (error) { - console.error('Post-response script error:', error); - postResponseError = error; - } - - if (postResponseScriptResult?.results) { - mainWindow.webContents.send('main:run-request-event', { - type: 'test-results-post-response', - results: postResponseScriptResult.results, - itemUid: item.uid, - requestUid, - collectionUid - }); - } - - !runInBackground && notifyScriptExecution({ - channel: 'main:run-request-event', - basePayload: { requestUid, collectionUid, itemUid: item.uid }, - scriptType: 'post-response', - error: postResponseError - }); - - // run assertions - const assertions = get(request, 'assertions'); - if (assertions) { - const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); - const results = assertRuntime.runAssertions( - assertions, - request, - response, - envVars, - runtimeVariables, - processEnvVars - ); - - !runInBackground && mainWindow.webContents.send('main:run-request-event', { - type: 'assertion-results', - results: results, - itemUid: item.uid, - requestUid, - collectionUid - }); - } - - const testFile = get(request, 'tests'); - const collectionName = collection?.name - if (typeof testFile === 'string') { - const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - let testResults = null; - let testError = null; - + const runPostScripts = async () => { try { - testResults = await testRuntime.runTests( - decomment(testFile), - request, + postResponseScriptResult = await runPostResponse(request, response, + requestUid, envVars, - runtimeVariables, collectionPath, - onConsoleLog, + collection, + collectionUid, + runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname, - collectionName - ); + runRequestByItemPathname); } catch (error) { - testError = error; - - if (error.partialResults) { - testResults = error.partialResults; - } else { - testResults = { - request, - envVariables: envVars, - runtimeVariables, - globalEnvironmentVariables: request?.globalEnvironmentVariables || {}, - results: [], - nextRequestName: null - }; - } + console.error('Post-response script error:', error); + postResponseError = error; } - !runInBackground && mainWindow.webContents.send('main:run-request-event', { - type: 'test-results', - results: testResults.results, - itemUid: item.uid, - requestUid, - collectionUid - }); - - mainWindow.webContents.send('main:script-environment-update', { - envVariables: testResults.envVariables, - runtimeVariables: testResults.runtimeVariables, - requestUid, - collectionUid - }); - - mainWindow.webContents.send('main:persistent-env-variables-update', { - persistentEnvVariables: testResults.persistentEnvVariables, - collectionUid - }); - - mainWindow.webContents.send('main:global-environment-variables-update', { - globalEnvironmentVariables: testResults.globalEnvironmentVariables - }); - - collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + if (postResponseScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-post-response', + results: postResponseScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } !runInBackground && notifyScriptExecution({ channel: 'main:run-request-event', basePayload: { requestUid, collectionUid, itemUid: item.uid }, - scriptType: 'test', - error: testError + scriptType: 'post-response', + error: postResponseError }); - const domainsWithCookiesTest = await getDomainsWithCookies(); - mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); - cookiesStore.saveCookieJar(); + // run assertions + const assertions = get(request, 'assertions'); + if (assertions) { + const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); + const results = assertRuntime.runAssertions(assertions, + request, + response, + envVars, + runtimeVariables, + processEnvVars); + + !runInBackground && mainWindow.webContents.send('main:run-request-event', { + type: 'assertion-results', + results: results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } + + const testFile = get(request, 'tests'); + const collectionName = collection?.name; + if (typeof testFile === 'string') { + const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); + let testResults = null; + let testError = null; + + try { + testResults = await testRuntime.runTests(decomment(testFile), + request, + response, + envVars, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runRequestByItemPathname, + collectionName); + } catch (error) { + testError = error; + + if (error.partialResults) { + testResults = error.partialResults; + } else { + testResults = { + request, + envVariables: envVars, + runtimeVariables, + globalEnvironmentVariables: request?.globalEnvironmentVariables || {}, + results: [], + nextRequestName: null + }; + } + } + + !runInBackground && mainWindow.webContents.send('main:run-request-event', { + type: 'test-results', + results: testResults.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + + mainWindow.webContents.send('main:script-environment-update', { + envVariables: testResults.envVariables, + runtimeVariables: testResults.runtimeVariables, + requestUid, + collectionUid + }); + + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: testResults.persistentEnvVariables, + collectionUid + }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: testResults.globalEnvironmentVariables + }); + + collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + + !runInBackground && notifyScriptExecution({ + channel: 'main:run-request-event', + basePayload: { requestUid, collectionUid, itemUid: item.uid }, + scriptType: 'test', + error: testError + }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); + cookiesStore.saveCookieJar(); + } + }; + + if (request.isStream) { + response.stream.on('close', () => runPostScripts().then()); + } else { + await runPostScripts(); } return { @@ -893,8 +926,8 @@ const registerNetworkIpc = (mainWindow) => { statusText: response.statusText, headers: response.headers, data: response.data, - dataBuffer: response.dataBuffer.toString('base64'), - size: Buffer.byteLength(response.dataBuffer), + stream: request.isStream ? response.stream : null, + cancelTokenUid: cancelTokenUid, duration: responseTime ?? 0, url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null, timeline: response.timeline @@ -917,7 +950,27 @@ const registerNetworkIpc = (mainWindow) => { const collectionUid = collection.uid; const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); - return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false }); + const response = await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false }); + if (response.stream) { + const stream = response.stream; + response.stream = undefined; + response.hasStreamRunning = response.status >= 200 && response.status < 300; + + stream.on('data', newData => { + const parsed = parseDataFromResponse({ data: newData, headers: {} }); + mainWindow.webContents.send('main:http-stream-new-data', {collectionUid, itemUid: item.uid, data: parsed}); + }); + + stream.on('close', () => { + if (!cancelTokens[response.cancelTokenUid]) { + return; + } + + mainWindow.webContents.send('main:http-stream-end', {collectionUid, itemUid: item.uid}); + deleteCancelToken(response.cancelTokenUid); + }); + } + return response; }); ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => { @@ -935,8 +988,9 @@ const registerNetworkIpc = (mainWindow) => { ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => { return new Promise((resolve, reject) => { if (cancelTokenUid && cancelTokens[cancelTokenUid]) { - cancelTokens[cancelTokenUid].abort(); + const abortController = cancelTokens[cancelTokenUid]; deleteCancelToken(cancelTokenUid); + abortController.abort(); // Ensure the on stream end event is called after the token is deleted resolve(); } else { reject(new Error('cancel token not found')); @@ -945,7 +999,7 @@ const registerNetworkIpc = (mainWindow) => { }); // handler for fetch-gql-schema - ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler) + ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler); ipcMain.handle( 'renderer:run-collection-folder', @@ -960,10 +1014,17 @@ const registerNetworkIpc = (mainWindow) => { const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); let stopRunnerExecution = false; + let currentAbortController; const abortController = new AbortController(); saveCancelToken(cancelTokenUid, abortController); + abortController.signal.addEventListener('abort', () => { + if (currentAbortController) { + currentAbortController.abort(); + } + }); + const runRequestByItemPathname = async (relativeItemPathname) => { return new Promise(async (resolve, reject) => { let itemPathname = path.join(collection?.pathname, relativeItemPathname); @@ -972,7 +1033,7 @@ const registerNetworkIpc = (mainWindow) => { } const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname)); if(_item) { - const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); + const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); resolve(res); } reject(`bru.runRequest: invalid request path - ${itemPathname}`); @@ -1004,9 +1065,8 @@ const registerNetworkIpc = (mainWindow) => { } }); - // sort requests by seq property - folderRequests = sortByNameThenSequence(folderRequests) + folderRequests = sortByNameThenSequence(folderRequests); } // Filter requests based on tags @@ -1015,7 +1075,7 @@ const registerNetworkIpc = (mainWindow) => { const excludeTags = tags.exclude ? tags.exclude : []; folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => { requestTags = draft?.tags || requestTags || []; - return isRequestTagsIncluded(requestTags, includeTags, excludeTags) + return isRequestTagsIncluded(requestTags, includeTags, excludeTags); }); } @@ -1068,15 +1128,14 @@ const registerNetworkIpc = (mainWindow) => { const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'runner'; - + const requestUid = uuid(); try { let preRequestScriptResult; let preRequestError = null; try { - preRequestScriptResult = await runPreRequest( - request, + preRequestScriptResult = await runPreRequest(request, requestUid, envVars, collectionPath, @@ -1085,8 +1144,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { console.error('Pre-request script error:', error); preRequestError = error; @@ -1154,8 +1212,9 @@ const registerNetworkIpc = (mainWindow) => { method: request.method, headers: headersSent, data: requestData, - dataBuffer: requestDataBuffer - } + dataBuffer: requestDataBuffer, + timestamp: Date.now() + }; // todo: // i have no clue why electron can't send the request object @@ -1166,9 +1225,11 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); - request.signal = abortController.signal; - const axiosInstance = await configureRequest( - collectionUid, + currentAbortController = new AbortController(); + request.signal = currentAbortController.signal; + request.responseType = 'stream'; + + const axiosInstance = await configureRequest(collectionUid, collection, request, envVars, @@ -1185,7 +1246,7 @@ const registerNetworkIpc = (mainWindow) => { collectionUid, credentialsId: request?.oauth2Credentials?.credentialsId, ...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }), - debugInfo: request?.oauth2Credentials?.debugInfo, + debugInfo: request?.oauth2Credentials?.debugInfo }); collection.oauth2Credentials = updateCollectionOauth2Credentials({ @@ -1213,6 +1274,10 @@ const registerNetworkIpc = (mainWindow) => { /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); + + request.isStream = isStream(response.headers); + response.data = await promisifyStream(response.data, currentAbortController, true); + timeEnd = Date.now(); const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); @@ -1254,6 +1319,9 @@ const registerNetworkIpc = (mainWindow) => { } if (error?.response) { + request.isStream = isStream(error.response.headers); + error.response.data = await promisifyStream(error.response.data, currentAbortController, true); + const { data, dataBuffer } = parseDataFromResponse(error.response); error.response.responseTime = error.response.headers.get('request-duration'); error.response.headers.delete('request-duration'); @@ -1270,7 +1338,7 @@ const registerNetworkIpc = (mainWindow) => { size: Buffer.byteLength(dataBuffer), data: error.response.data, responseTime: error.response.responseTime, - timeline: error.response.timeline, + timeline: error.response.timeline }; // if we get a response from the server, we consider it as a success @@ -1291,8 +1359,7 @@ const registerNetworkIpc = (mainWindow) => { let postResponseScriptResult; let postResponseError = null; try { - postResponseScriptResult = await runPostResponse( - request, + postResponseScriptResult = await runPostResponse(request, response, requestUid, envVars, @@ -1302,8 +1369,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { console.error('Post-response script error:', error); postResponseError = error; @@ -1340,14 +1406,12 @@ const registerNetworkIpc = (mainWindow) => { const assertions = get(item, 'request.assertions'); if (assertions) { const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); - const results = assertRuntime.runAssertions( - assertions, + const results = assertRuntime.runAssertions(assertions, request, response, envVars, runtimeVariables, - processEnvVars - ); + processEnvVars); mainWindow.webContents.send('main:run-folder-event', { type: 'assertion-results', @@ -1358,15 +1422,14 @@ const registerNetworkIpc = (mainWindow) => { } const testFile = get(request, 'tests'); - const collectionName = collection?.name + const collectionName = collection?.name; if (typeof testFile === 'string') { let testResults = null; let testError = null; try { const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - testResults = await testRuntime.runTests( - decomment(testFile), + testResults = await testRuntime.runTests(decomment(testFile), request, response, envVars, @@ -1376,11 +1439,10 @@ const registerNetworkIpc = (mainWindow) => { processEnvVars, scriptingConfig, runRequestByItemPathname, - collectionName - ); + collectionName); } catch (error) { testError = error; - + if (error.partialResults) { testResults = error.partialResults; } else { @@ -1414,7 +1476,7 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: testResults.globalEnvironmentVariables }); - + collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; notifyScriptExecution({ @@ -1443,7 +1505,7 @@ const registerNetworkIpc = (mainWindow) => { collectionUid, folderUid, statusText: 'collection run was terminated!', - runCompletionTime: new Date().toISOString(), + runCompletionTime: new Date().toISOString() }); break; } @@ -1460,7 +1522,7 @@ const registerNetworkIpc = (mainWindow) => { if (nextRequestIdx >= 0) { currentRequestIndex = nextRequestIdx; } else { - console.error("Could not find request with name '" + nextRequestName + "'"); + console.error('Could not find request with name \'' + nextRequestName + '\''); currentRequestIndex++; } } else { @@ -1473,10 +1535,10 @@ const registerNetworkIpc = (mainWindow) => { type: 'testrun-ended', collectionUid, folderUid, - runCompletionTime: new Date().toISOString(), + runCompletionTime: new Date().toISOString() }); } catch (error) { - console.log("error", error); + console.log('error', error); deleteCancelToken(cancelTokenUid); mainWindow.webContents.send('main:run-folder-event', { type: 'testrun-ended', @@ -1573,14 +1635,13 @@ const executeRequestOnFailHandler = async (request, error) => { } }; - const registerAllNetworkIpc = (mainWindow) => { registerNetworkIpc(mainWindow); registerGrpcEventHandlers(mainWindow); registerWsEventHandlers(mainWindow); -} +}; -module.exports = registerAllNetworkIpc +module.exports = registerAllNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;