Skip to content

Commit 5e8e234

Browse files
committed
added Tests support for Grpc
1 parent 6c7bada commit 5e8e234

6 files changed

Lines changed: 148 additions & 9 deletions

File tree

packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import StyledWrapper from './StyledWrapper';
1515
import { hasEffectiveAuth } from 'utils/auth';
1616
import { AUTH_MODES_GRPC } from 'utils/common/constants';
1717
import Script from 'components/RequestPane/Script';
18+
import Tests from 'components/RequestPane/Tests';
1819

1920
const GrpcRequestPane = ({ item, collection, handleRun }) => {
2021
const dispatch = useDispatch();
@@ -50,6 +51,9 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
5051
case 'scripts': {
5152
return <Script protocol="grpc" item={item} collection={collection} />;
5253
}
54+
case 'tests': {
55+
return <Tests item={item} collection={collection} protocol="grpc" />;
56+
}
5357
default: {
5458
return <div className="mt-4">404 | Not found</div>;
5559
}
@@ -59,6 +63,10 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
5963
const body = getPropertyFromDraftOrRequest(item, 'request.body');
6064
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
6165
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
66+
const script = getPropertyFromDraftOrRequest(item, 'request.script');
67+
const tests = getPropertyFromDraftOrRequest(item, 'request.tests');
68+
const hasTestError = item.testScriptErrorMessage;
69+
6270
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
6371
const hasAuth = useMemo(
6472
() => hasEffectiveAuth(collection, item, AUTH_MODES_GRPC),
@@ -72,6 +80,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
7280
const isClientStreaming = request.methodType === 'client-streaming' || request.methodType === 'bidi-streaming';
7381

7482
const allTabs = useMemo(() => {
83+
const hasScriptError = item.preRequestScriptErrorMessage || item.onMessageScriptErrorMessage || item.postResponseScriptErrorMessage;
7584
const getMessageIndicator = () => {
7685
if (grpcMessagesCount > 0) {
7786
return isClientStreaming ? (
@@ -107,10 +116,15 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
107116
{
108117
key: 'scripts',
109118
label: 'Scripts',
110-
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
119+
indicator: (script.req || script.stream || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null
120+
},
121+
{
122+
key: 'tests',
123+
label: 'Tests',
124+
indicator: tests && tests.length > 0 ? hasTestError ? <StatusDot type="error" /> : <StatusDot type="default" /> : null
111125
}
112126
];
113-
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, hasAuth, docs]);
127+
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, hasAuth, tests, hasTestError, docs, item.preRequestScriptErrorMessage, item.onMessageScriptErrorMessage, item.postResponseScriptErrorMessage]);
114128

115129
// Initialize tab to 'body' if no tab is currently set
116130
useEffect(() => {

packages/bruno-app/src/components/RequestPane/Script/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ const Script = ({ item, collection, protocol = 'http' }) => {
208208
className="mt-2"
209209
dataTestId={`${phase.key}-script-editor`}
210210
>
211-
{renderEditor(phase)}
211+
{activeTab === phase.key ? renderEditor(phase) : null}
212212
</TabsContent>
213213
))}
214214
</Tabs>

packages/bruno-app/src/components/RequestPane/Tests/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import { useTheme } from 'providers/Theme';
1010
import { usePersistedState } from 'hooks/usePersistedState';
1111
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
1212

13-
const Tests = ({ item, collection }) => {
13+
/**
14+
* @typedef {Object} TestsProps
15+
* @property {Object} item - The request item (http or grpc).
16+
* @property {Object} collection - The collection the item belongs to.
17+
* @property {'http' | 'grpc'} [protocol] - Request protocol; defaults to 'http'.
18+
*/
19+
20+
/** @param {TestsProps} props */
21+
const Tests = ({ item, collection, protocol = 'http' }) => {
1422
const dispatch = useDispatch();
1523
const testsEditorRef = useRef(null);
1624
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
@@ -46,6 +54,7 @@ const Tests = ({ item, collection }) => {
4654
ref={testsEditorRef}
4755
collection={collection}
4856
item={item}
57+
protocol={protocol}
4958
docKey="tests"
5059
value={tests || ''}
5160
theme={displayedTheme}

packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import ResponseLayoutToggle from '../ResponseLayoutToggle';
1818
import ResponsiveTabs from 'ui/ResponsiveTabs';
1919
import ScriptError from '../ScriptError';
2020
import ScriptErrorIcon from '../ScriptErrorIcon';
21+
import TestResults from '../TestResults';
22+
import TestResultsLabel from '../TestResultsLabel';
2123

2224
const GrpcResponsePane = ({ item, collection }) => {
2325
const dispatch = useDispatch();
@@ -80,6 +82,18 @@ const GrpcResponsePane = ({ item, collection }) => {
8082
key: 'timeline',
8183
label: 'Timeline',
8284
indicator: null
85+
},
86+
{
87+
key: 'tests',
88+
label: (
89+
<TestResultsLabel
90+
results={item.testResults}
91+
assertionResults={item.assertionResults}
92+
preRequestTestResults={item.preRequestTestResults}
93+
postResponseTestResults={item.postResponseTestResults}
94+
/>
95+
),
96+
indicator: null
8397
}
8498
];
8599

@@ -97,6 +111,17 @@ const GrpcResponsePane = ({ item, collection }) => {
97111
case 'timeline': {
98112
return <Timeline collection={collection} item={item} activeTabUid={activeTabUid} />;
99113
}
114+
case 'tests': {
115+
return (
116+
<TestResults
117+
item={item}
118+
results={item.testResults}
119+
assertionResults={item.assertionResults}
120+
preRequestTestResults={item.preRequestTestResults}
121+
postResponseTestResults={item.postResponseTestResults}
122+
/>
123+
);
124+
}
100125
default: {
101126
return <div>404 | Not found</div>;
102127
}

packages/bruno-electron/src/ipc/network/grpc-event-handlers.js

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// To implement grpc event handlers
22
const { ipcMain, app } = require('electron');
33
const { GrpcClient } = require('@usebruno/requests');
4-
const { ScriptRuntime, formatErrorWithContextV2 } = require('@usebruno/js');
4+
const { ScriptRuntime, TestRuntime, formatErrorWithContextV2 } = require('@usebruno/js');
55
const decomment = require('decomment');
66
const { safeParseJSON, safeStringifyJSON } = require('../../utils/common');
77
const { cloneDeep, get } = require('lodash');
@@ -365,6 +365,85 @@ const registerGrpcEventHandlers = (window) => {
365365
return { scriptResult, scriptError };
366366
};
367367

368+
/**
369+
* Run the gRPC request tests (the `tests` block) once the call terminates.
370+
* Mirrors the HTTP post-response test execution: runs the test source stored in
371+
* the .bru file against the collected responses, emits the `test-results` event,
372+
* and surfaces any test script error via a `test-script-execution` event so the
373+
* existing ScriptError card renders for gRPC too.
374+
*
375+
* @returns {{ testResults: object | null, testError: Error | null }}
376+
*/
377+
const runResponseTests = async ({
378+
request,
379+
collection,
380+
envVars,
381+
runtimeVariables,
382+
processEnvVars,
383+
scriptingConfig,
384+
requestUid,
385+
itemUid,
386+
responses
387+
}) => {
388+
const testFile = get(request, 'tests');
389+
if (typeof testFile !== 'string' || !testFile.length) {
390+
return { testResults: null, testError: null };
391+
}
392+
393+
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
394+
let testResults = null;
395+
let testError = null;
396+
try {
397+
testResults = await testRuntime.runTests(
398+
decomment(testFile, { space: true }),
399+
request,
400+
{ responses: responses || [] },
401+
envVars,
402+
runtimeVariables,
403+
collection.pathname,
404+
onConsoleLog,
405+
processEnvVars,
406+
scriptingConfig,
407+
null,
408+
collection.name
409+
);
410+
} catch (error) {
411+
testError = error;
412+
// Preserve any test() calls that passed before the script errored
413+
testResults = error.partialResults || {
414+
request,
415+
envVariables: envVars,
416+
runtimeVariables,
417+
globalEnvironmentVariables: request?.globalEnvironmentVariables || {},
418+
results: [],
419+
nextRequestName: null
420+
};
421+
}
422+
423+
sendEvent('main:run-request-event', {
424+
type: 'test-results',
425+
results: testResults.results,
426+
requestUid,
427+
itemUid,
428+
collectionUid: collection.uid
429+
});
430+
431+
sendEvent('main:run-request-event', {
432+
type: 'test-script-execution',
433+
requestUid,
434+
itemUid,
435+
collectionUid: collection.uid,
436+
errorMessage: testError ? (testError.message || 'An error occurred while executing the test script') : null,
437+
errorContext: testError
438+
? formatErrorWithContextV2(testError, 'test', request?.testsMetadata, collection.pathname)
439+
: null
440+
});
441+
442+
propagateScriptEnvUpdates(testResults, request, collection);
443+
444+
return { testResults, testError };
445+
};
446+
368447
ipcMain.handle('connections-changed', (event) => {
369448
sendEvent('grpc:connections-changed', event);
370449
});
@@ -416,13 +495,23 @@ const registerGrpcEventHandlers = (window) => {
416495
}
417496
: undefined;
418497

419-
// 3. After Response — script.res (fires once on terminal event).
498+
// 3. After Response — script.res then tests (fire once on terminal event).
420499
// `responses` is the full list of received messages, collected by the gRPC client.
421-
const onAfterResponse = preparedRequest?.script?.res?.length
500+
const hasAfterResponseScript = !!preparedRequest?.script?.res?.length;
501+
const afterResponseTests = get(preparedRequest, 'tests');
502+
const hasTests = typeof afterResponseTests === 'string' && afterResponseTests.length > 0;
503+
const onAfterResponse = (hasAfterResponseScript || hasTests)
422504
? (responses) => {
423505
if (onMessageErrored) return;
424-
runAfterResponseScript({ ...scriptContext, responses }).catch((err) => {
425-
console.error('Error running gRPC after-response script:', err);
506+
(async () => {
507+
if (hasAfterResponseScript) {
508+
await runAfterResponseScript({ ...scriptContext, responses });
509+
}
510+
if (hasTests) {
511+
await runResponseTests({ ...scriptContext, responses });
512+
}
513+
})().catch((err) => {
514+
console.error('Error running gRPC after-response script/tests:', err);
426515
});
427516
}
428517
: undefined;

packages/bruno-electron/src/ipc/network/prepare-grpc-request.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
161161
body: request.body,
162162
protoPath: request.protoPath,
163163
script: request.script,
164+
tests: request.tests,
165+
testsMetadata: request.testsMetadata,
164166
// Add variable properties for interpolation
165167
vars: request.vars,
166168
collectionVariables: request.collectionVariables,

0 commit comments

Comments
 (0)