diff --git a/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/StyledWrapper.js index d78558bf79..0b49d66ca4 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 0000000000..1b5901977b --- /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 0000000000..7f66d78857 --- /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} +
+ +
+ ); +}; + +export default TestResults; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index eca996f754..79b64db2d3 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 17518351fd..55dbd1d6d7 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 a8d3c025da..a87db2ebb1 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 cb29fd17b2..9c994c66a6 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 fb6618adab..be5888f6ad 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 1a5001f1bb..31ea7bde43 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 ec3f434774..904e2393d9 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 0000000000..17d35fe4fb --- /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 0000000000..59c45331bb --- /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 0000000000..975746bd2f --- /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