diff --git a/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js
index d78558bf7..0b49d66ca 100644
--- a/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js
@@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
}
}
}
+
+ .some-tests-failed {
+ color: ${(props) => props.theme.colors.text.danger} !important;
+ }
+
+ .all-tests-passed {
+ color: ${(props) => props.theme.colors.text.green} !important;
+ }
`;
export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js
new file mode 100644
index 000000000..1b5901977
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js
@@ -0,0 +1,17 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .test-success {
+ color: ${(props) => props.theme.colors.text.green};
+ }
+
+ .test-failure {
+ color: ${(props) => props.theme.colors.text.danger};
+
+ .error-message {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js
new file mode 100644
index 000000000..7f66d7885
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import StyledWrapper from './StyledWrapper';
+
+const TestResults = ({ results }) => {
+ if (!results || !results.length) {
+ return (
+
+ No tests found
+
+ );
+ }
+
+ const passedTests = results.filter((result) => result.status === 'pass');
+ const failedTests = results.filter((result) => result.status === 'fail');
+
+ return (
+
+
+ Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
+
+
+ {results.map((result, index) => (
+ -
+ {result.status === 'pass' ? (
+
+ ✔ {result.description}
+
+ ) : (
+ <>
+
+ ✘ {result.description}
+
+
+
+ {result.error}
+
+ >
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default TestResults;
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index eca996f75..79b64db2d 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -11,8 +11,33 @@ import StatusCode from './StatusCode';
import ResponseTime from './ResponseTime';
import ResponseSize from './ResponseSize';
import Timeline from './Timeline';
+import TestResults from './TestResults';
import StyledWrapper from './StyledWrapper';
+const TestResultsLabel = ({ results }) => {
+ if(!results || !results.length) {
+ return 'Tests';
+ }
+
+ const numberOfTests = results.length;
+ const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
+
+ return (
+
+
Tests
+ {numberOfFailedTests ? (
+
+ {numberOfFailedTests}
+
+ ) : (
+
+ {numberOfTests}
+
+ )}
+
+ );
+};
+
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -46,6 +71,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
case 'timeline': {
return ;
}
+ case 'tests': {
+ return ;
+ }
default: {
return 404 | Not found
;
@@ -96,6 +124,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
selectTab('timeline')}>
Timeline
+ selectTab('tests')}>
+
+
{!isLoading ? (
diff --git a/packages/bruno-app/src/providers/App/useCollectionTreeSync.js b/packages/bruno-app/src/providers/App/useCollectionTreeSync.js
index 17518351f..55dbd1d6d 100644
--- a/packages/bruno-app/src/providers/App/useCollectionTreeSync.js
+++ b/packages/bruno-app/src/providers/App/useCollectionTreeSync.js
@@ -9,6 +9,7 @@ import {
collectionUnlinkEnvFileEvent,
requestSentEvent,
requestQueuedEvent,
+ testResultsEvent,
scriptEnvironmentUpdateEvent
} from 'providers/ReduxStore/slices/collections';
import toast from 'react-hot-toast';
@@ -95,6 +96,10 @@ const useCollectionTreeSync = () => {
dispatch(requestQueuedEvent(val));
};
+ const _testResults = (val) => {
+ dispatch(testResultsEvent(val));
+ };
+
ipcRenderer.invoke('renderer:ready');
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
@@ -104,6 +109,7 @@ const useCollectionTreeSync = () => {
const removeListener5 = ipcRenderer.on('main:http-request-sent', _httpRequestSent);
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
+ const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
return () => {
removeListener1();
@@ -113,6 +119,7 @@ const useCollectionTreeSync = () => {
removeListener5();
removeListener6();
removeListener7();
+ removeListener8();
};
}, [isElectron]);
};
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 a8d3c025d..a87db2ebb 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -812,6 +812,18 @@ export const collectionsSlice = createSlice({
collection.environments.push(environment);
}
}
+ },
+ testResultsEvent: (state, action) => {
+ const { itemUid, collectionUid, results } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, itemUid);
+
+ if (item) {
+ item.testResults = results;
+ }
+ }
}
}
});
@@ -861,7 +873,8 @@ export const {
collectionChangeFileEvent,
collectionUnlinkFileEvent,
collectionUnlinkDirectoryEvent,
- collectionAddEnvFileEvent
+ collectionAddEnvFileEvent,
+ testResultsEvent
} = collectionsSlice.actions;
export default collectionsSlice.reducer;
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index cb29fd17b..9c994c66a 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -20,6 +20,7 @@
"atob": "^2.1.2",
"axios": "^0.26.0",
"btoa": "^1.2.1",
+ "chai": "^4.3.7",
"chokidar": "^3.5.3",
"crypto-js": "^4.1.1",
"electron-is-dev": "^2.0.0",
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index fb6618ada..be5888f6a 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -2,8 +2,8 @@ const axios = require('axios');
const Mustache = require('mustache');
const FormData = require('form-data');
const { ipcMain } = require('electron');
-const { forOwn, extend, each } = require('lodash');
-const { ScriptRuntime } = require('@usebruno/js');
+const { forOwn, extend, each, get } = require('lodash');
+const { ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common');
@@ -79,12 +79,12 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
const envVars = getEnvVars(environment);
if(request.script && request.script.length) {
- let script = request.script + '\n if (typeof onRequest === "function") {onRequest(brunoRequest);}';
+ let script = request.script + '\n if (typeof onRequest === "function") {onRequest(__brunoRequest);}';
const scriptRuntime = new ScriptRuntime();
- const res = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
+ const result = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
mainWindow.webContents.send('main:script-environment-update', {
- environment: res.environment,
+ environment: result.environment,
collectionUid
});
}
@@ -106,15 +106,27 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
cancelTokenUid
});
- const result = await axios(request);
+ const response = await axios(request);
if(request.script && request.script.length) {
- let script = request.script + '\n if (typeof onResponse === "function") {onResponse(brunoResponse);}';
+ let script = request.script + '\n if (typeof onResponse === "function") {onResponse(__brunoResponse);}';
const scriptRuntime = new ScriptRuntime();
- const res = scriptRuntime.runResponseScript(script, result, envVars, collectionPath);
+ const result = scriptRuntime.runResponseScript(script, response, envVars, collectionPath);
mainWindow.webContents.send('main:script-environment-update', {
- environment: res.environment,
+ environment: result.environment,
+ collectionUid
+ });
+ }
+
+ const testFile = get(item, 'request.tests');
+ if(testFile && testFile.length) {
+ const testRuntime = new TestRuntime();
+ const result = testRuntime.runTests(testFile, request, response, envVars, collectionPath);
+
+ mainWindow.webContents.send('main:test-results', {
+ results: result.results,
+ itemUid: item.uid,
collectionUid
});
}
@@ -122,10 +134,10 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
deleteCancelToken(cancelTokenUid);
return {
- status: result.status,
- statusText: result.statusText,
- headers: result.headers,
- data: result.data
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers,
+ data: response.data
};
} catch (error) {
// todo: better error handling
diff --git a/packages/bruno-js/src/index.js b/packages/bruno-js/src/index.js
index 1a5001f1b..31ea7bde4 100644
--- a/packages/bruno-js/src/index.js
+++ b/packages/bruno-js/src/index.js
@@ -2,6 +2,11 @@ const {
ScriptRuntime
} = require('./scripts/script-runtime');
+const {
+ TestRuntime
+} = require('./scripts/test-runtime');
+
module.exports = {
- ScriptRuntime
+ ScriptRuntime,
+ TestRuntime
};
diff --git a/packages/bruno-js/src/scripts/script-runtime.js b/packages/bruno-js/src/scripts/script-runtime.js
index ec3f43477..904e2393d 100644
--- a/packages/bruno-js/src/scripts/script-runtime.js
+++ b/packages/bruno-js/src/scripts/script-runtime.js
@@ -10,11 +10,11 @@ class ScriptRuntime {
runRequestScript(script, request, environment, collectionPath) {
const bru = new Bru(environment);
- const brunoRequest = new BrunoRequest(request);
+ const __brunoRequest = new BrunoRequest(request);
const context = {
bru,
- brunoRequest
+ __brunoRequest
};
const vm = new NodeVM({
sandbox: context,
@@ -35,11 +35,11 @@ class ScriptRuntime {
runResponseScript(script, response, environment, collectionPath) {
const bru = new Bru(environment);
- const brunoResponse = new BrunoResponse(response);
+ const __brunoResponse = new BrunoResponse(response);
const context = {
bru,
- brunoResponse
+ __brunoResponse
};
const vm = new NodeVM({
sandbox: context,
diff --git a/packages/bruno-js/src/scripts/test-results.js b/packages/bruno-js/src/scripts/test-results.js
new file mode 100644
index 000000000..17d35fe4f
--- /dev/null
+++ b/packages/bruno-js/src/scripts/test-results.js
@@ -0,0 +1,15 @@
+class TestResults {
+ constructor() {
+ this.results = [];
+ }
+
+ addResult(result) {
+ this.results.push(result);
+ }
+
+ getResults() {
+ return this.results;
+ }
+}
+
+module.exports = TestResults;
diff --git a/packages/bruno-js/src/scripts/test-runtime.js b/packages/bruno-js/src/scripts/test-runtime.js
new file mode 100644
index 000000000..59c45331b
--- /dev/null
+++ b/packages/bruno-js/src/scripts/test-runtime.js
@@ -0,0 +1,55 @@
+const { NodeVM } = require('vm2');
+const chai = require('chai');
+const path = require('path');
+const Bru = require('./bru');
+const BrunoRequest = require('./bruno-request');
+const BrunoResponse = require('./bruno-response');
+const Test = require('./test');
+const TestResults = require('./test-results');
+
+class TestRuntime {
+ constructor() {
+ }
+
+ runTests(testsFile, request, response, environment, collectionPath) {
+ const bru = new Bru(environment);
+ const req = new BrunoRequest(request);
+ const res = new BrunoResponse(response);
+
+ const __brunoTestResults = new TestResults();
+ const test = Test(__brunoTestResults, chai);
+
+ const context = {
+ bru,
+ req,
+ res,
+ test,
+ expect: chai.expect,
+ assert: chai.assert,
+ __brunoTestResults: __brunoTestResults
+ };
+
+ const vm = new NodeVM({
+ sandbox: context,
+ require: {
+ context: 'sandbox',
+ external: true,
+ root: [collectionPath]
+ }
+ });
+ console.log(__brunoTestResults);
+
+ vm.run(testsFile, path.join(collectionPath, 'vm.js'));
+
+ return {
+ request,
+ response,
+ environment,
+ results: __brunoTestResults.getResults()
+ };
+ }
+}
+
+module.exports = {
+ TestRuntime
+};
diff --git a/packages/bruno-js/src/scripts/test.js b/packages/bruno-js/src/scripts/test.js
new file mode 100644
index 000000000..975746bd2
--- /dev/null
+++ b/packages/bruno-js/src/scripts/test.js
@@ -0,0 +1,27 @@
+const Test = (__brunoTestResults, chai) => (description, callback) => {
+ try {
+ callback();
+ __brunoTestResults.addResult({ description, status: "pass" });
+ } catch (error) {
+ console.log(chai.AssertionError);
+ if (error instanceof chai.AssertionError) {
+ const { message, actual, expected } = error;
+ __brunoTestResults.addResult({
+ description,
+ status: "fail",
+ error: message,
+ actual,
+ expected
+ });
+ } else {
+ __brunoTestResults.addResult({
+ description,
+ status: "fail",
+ error: error.message || 'An unexpected error occurred.'
+ });
+ }
+ console.log(error);
+ }
+}
+
+module.exports = Test;
\ No newline at end of file