From 8044286b806f75a3811916de46059e37b6958ff6 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Wed, 22 Feb 2023 02:25:02 +0530 Subject: [PATCH] feat: integrated assert runtime for ui --- .../Assertions/AssertionOperator/index.js | 8 --- .../Assertions/AssertionRow/index.js | 2 - .../ResponsePane/TestResults/StyledWrapper.js | 6 +- .../ResponsePane/TestResults/index.js | 34 ++++++++++- .../ResponsePane/TestResultsLabel/index.js | 18 ++++-- .../src/components/ResponsePane/index.js | 4 +- .../src/components/RunnerResults/index.js | 38 +++++++++++- .../providers/App/useCollectionTreeSync.js | 11 +++- .../ReduxStore/slices/collections/index.js | 19 ++++++ .../bruno-electron/src/ipc/network/index.js | 36 ++++++++++- .../bruno-js/src/runtime/assert-runtime.js | 59 ++++++++++++++++++- 11 files changed, 204 insertions(+), 31 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js index 993eb791..e5895973 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js @@ -49,14 +49,6 @@ const AssertionOperator = ({ operator, onChange }) => { return 'equals'; case 'neq': return 'notEquals'; - case 'gt': - return 'greaterThan'; - case 'gte': - return 'greaterThanOrEqual'; - case 'lt': - return 'lessThan'; - case 'lte': - return 'lessThanOrEqual'; default: return operator; } diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js index bbf18ad0..201a47c9 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js @@ -94,8 +94,6 @@ const AssertionRow = ({ operator, value } = parseAssertionOperator(assertion.value); - console.log(operator); - console.log(value); return ( diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js index 1b590197..13fa4114 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js @@ -7,10 +7,10 @@ const StyledWrapper = styled.div` .test-failure { color: ${(props) => props.theme.colors.text.danger}; + } - .error-message { - color: ${(props) => props.theme.colors.text.muted}; - } + .error-message { + color: ${(props) => props.theme.colors.text.muted}; } `; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js index 4dfa9cf4..10e7d3af 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js @@ -1,8 +1,10 @@ import React from 'react'; import StyledWrapper from './StyledWrapper'; -const TestResults = ({ results }) => { - if (!results || !results.length) { +const TestResults = ({ results, assertionResults }) => { + results = results || []; + assertionResults = assertionResults || []; + if (!results.length && !assertionResults.length) { return (
No tests found @@ -13,6 +15,9 @@ const TestResults = ({ results }) => { const passedTests = results.filter((result) => result.status === 'pass'); const failedTests = results.filter((result) => result.status === 'fail'); + const passedAssertions = assertionResults.filter((result) => result.status === 'pass'); + const failedAssertions = assertionResults.filter((result) => result.status === 'fail'); + return (
@@ -39,6 +44,31 @@ const TestResults = ({ results }) => { ))} + +
+ Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed: {failedAssertions.length} +
+
    + {assertionResults.map((result) => ( +
  • + {result.status === 'pass' ? ( + + ✔  {result.lhsExpr}: {result.rhsExpr} + + ) : ( + <> + + ✘  {result.lhsExpr}: {result.rhsExpr} + +
    + + {result.error} + + + )} +
  • + ))} +
); }; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js index 65181992..6cf9a57b 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js @@ -1,23 +1,31 @@ import React from 'react'; -const TestResultsLabel = ({ results }) => { - if(!results || !results.length) { +const TestResultsLabel = ({ results, assertionResults }) => { + results = results || []; + assertionResults = assertionResults || []; + if(!results.length && !assertionResults.length) { return 'Tests'; } const numberOfTests = results.length; const numberOfFailedTests = results.filter(result => result.status === 'fail').length; + const numberOfAssertions = assertionResults.length; + const numberOfFailedAssertions = assertionResults.filter(result => result.status === 'fail').length; + + const totalNumberOfTests = numberOfTests + numberOfAssertions; + const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions; + return (
Tests
- {numberOfFailedTests ? ( + {totalNumberOfFailedTests ? ( - {numberOfFailedTests} + {totalNumberOfFailedTests} ) : ( - {numberOfTests} + {totalNumberOfTests} )}
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 7a62b3b6..1648c708 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -50,7 +50,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { return ; } case 'tests': { - return ; + return ; } default: { @@ -103,7 +103,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { Timeline
selectTab('tests')}> - +
{!isLoading ? (
diff --git a/packages/bruno-app/src/components/RunnerResults/index.js b/packages/bruno-app/src/components/RunnerResults/index.js index c174609c..68d686af 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.js +++ b/packages/bruno-app/src/components/RunnerResults/index.js @@ -50,6 +50,14 @@ export default function RunnerResults({collection}) { } else { item.testStatus = 'pass'; } + + if(item.assertionResults) { + const failed = item.assertionResults.filter((result) => result.status === 'fail'); + + item.assertionStatus = failed.length ? 'fail' : 'pass'; + } else { + item.assertionStatus = 'pass'; + } } }); @@ -68,8 +76,12 @@ export default function RunnerResults({collection}) { }; const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy); - const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass'); - const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail'); + const passedRequests = items.filter((item) => { + return item.status !== "error" && item.testStatus === 'pass' && item.assertionStatus === 'pass'; + }); + const failedRequests = items.filter((item) => { + return item.status !== "error" && item.testStatus === 'fail' || item.assertionStatus === 'fail'; + }); if(!items || !items.length) { return ( @@ -139,7 +151,7 @@ export default function RunnerResults({collection}) {
    {item.testResults ? item.testResults.map((result) => ( -
  • +
  • {result.status === 'pass' ? ( @@ -158,6 +170,26 @@ export default function RunnerResults({collection}) { )}
  • )): null} + {item.assertionResults ? item.assertionResults.map((result) => ( +
  • + {result.status === 'pass' ? ( + + + {result.lhsExpr}: {result.rhsExpr} + + ) : ( + <> + + + {result.lhsExpr}: {result.rhsExpr} + + + {result.error} + + + )} +
  • + )): null}
diff --git a/packages/bruno-app/src/providers/App/useCollectionTreeSync.js b/packages/bruno-app/src/providers/App/useCollectionTreeSync.js index b7663adb..1d56f2c7 100644 --- a/packages/bruno-app/src/providers/App/useCollectionTreeSync.js +++ b/packages/bruno-app/src/providers/App/useCollectionTreeSync.js @@ -10,6 +10,7 @@ import { requestSentEvent, requestQueuedEvent, testResultsEvent, + assertionResultsEvent, scriptEnvironmentUpdateEvent, collectionRenamedEvent, runFolderEvent @@ -111,6 +112,10 @@ const useCollectionTreeSync = () => { dispatch(testResultsEvent(val)); }; + const _assertionResults = (val) => { + dispatch(assertionResultsEvent(val)); + }; + const _collectionRenamed = (val) => { dispatch(collectionRenamedEvent(val)); }; @@ -129,8 +134,9 @@ const useCollectionTreeSync = () => { 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); - const removeListener9 = ipcRenderer.on('main:collection-renamed', _collectionRenamed); - const removeListener10 = ipcRenderer.on('main:run-folder-event', _runFolderEvent); + const removeListener9 = ipcRenderer.on('main:assertion-results', _assertionResults); + const removeListener10 = ipcRenderer.on('main:collection-renamed', _collectionRenamed); + const removeListener11 = ipcRenderer.on('main:run-folder-event', _runFolderEvent); return () => { removeListener1(); @@ -143,6 +149,7 @@ const useCollectionTreeSync = () => { removeListener8(); removeListener9(); removeListener10(); + removeListener11(); }; }, [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 75f55643..46606253 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1022,6 +1022,19 @@ export const collectionsSlice = createSlice({ } } }, + assertionResultsEvent: (state, action) => { + const { itemUid, collectionUid, results } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + console.log(results); + + if (collection) { + const item = findItemInCollection(collection, itemUid); + + if (item) { + item.assertionResults = results; + } + } + }, collectionRenamedEvent: (state, action) => { const { collectionPathname, newName } = action.payload; const collection = findCollectionByPathname(state.collections, collectionPathname); @@ -1112,6 +1125,11 @@ export const collectionsSlice = createSlice({ item.testResults = action.payload.testResults; } + if(type === 'assertion-results') { + const item = collection.runnerResult.items.find((i) => i.uid === request.uid); + item.assertionResults = action.payload.assertionResults; + } + if(type === 'error') { const item = collection.runnerResult.items.find((i) => i.uid === request.uid); item.error = action.payload.error; @@ -1187,6 +1205,7 @@ export const { collectionUnlinkDirectoryEvent, collectionAddEnvFileEvent, testResultsEvent, + assertionResultsEvent, collectionRenamedEvent, toggleRunnerView, showRunnerView, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 0e669fef..2b29c2d8 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -167,6 +167,19 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => { }); } + // run assertions + const assertions = get(request, 'assertions'); + if(assertions && assertions.length) { + const assertRuntime = new AssertRuntime(); + const results = assertRuntime.runAssertions(assertions, request, response, envVars, collectionVariables, collectionPath); + + mainWindow.webContents.send('main:assertion-results', { + results: results, + itemUid: item.uid, + collectionUid + }); + } + // run tests const testFile = get(item, 'request.tests'); if(testFile && testFile.length) { @@ -359,7 +372,13 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => { const postResponseVars = get(request, 'vars.res', []); if(postResponseVars && postResponseVars.length) { const varsRuntime = new VarsRuntime(); - varsRuntime.runPostResponseVars(postResponseVars, request, response, envVars, collectionVariables, collectionPath); + const result = varsRuntime.runPostResponseVars(postResponseVars, request, response, envVars, collectionVariables, collectionPath); + + mainWindow.webContents.send('main:script-environment-update', { + envVariables: result.envVariables, + collectionVariables: result.collectionVariables, + collectionUid + }); } // run response script @@ -375,6 +394,21 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => { }); } + // run assertions + const assertions = get(item, 'request.assertions'); + if(assertions && assertions.length) { + const assertRuntime = new AssertRuntime(); + const results = assertRuntime.runAssertions(assertions, request, response, envVars, collectionVariables, collectionPath); + + mainWindow.webContents.send('main:run-folder-event', { + type: 'assertion-results', + assertionResults: results, + itemUid: item.uid, + collectionUid + }); + } + + // run tests const testFile = get(item, 'request.tests'); if(testFile && testFile.length) { const testRuntime = new TestRuntime(); diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index 89ccddfb..eca4f845 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -1,8 +1,9 @@ const _ = require('lodash'); -const chai = require('chai'); +const chai = require('chai'); +const { nanoid } = require('nanoid'); const Bru = require('../bru'); const BrunoRequest = require('../bruno-request'); -const { evaluateJsExpression, createResponseParser } = require('../utils'); +const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils'); const { expect } = chai; @@ -51,9 +52,20 @@ const parseAssertionOperator = (str = '') => { 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean' ]; + const unaryOperators = [ + 'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean' + ]; + const [operator, ...rest] = str.trim().split(' '); const value = rest.join(' '); + if(unaryOperators.includes(operator)) { + return { + operator, + value: '' + }; + } + if(operators.includes(operator)) { return { operator, @@ -67,6 +79,45 @@ const parseAssertionOperator = (str = '') => { }; }; +const isUnaryOperator = (operator) => { + const unaryOperators = [ + 'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean' + ]; + + return unaryOperators.includes(operator); +}; + +const evaluateRhsOperand = (rhsOperand, operator, context) => { + if(isUnaryOperator(operator)) { + return; + } + + // gracefulle allyow both a,b as well as [a, b] + if(operator === 'in' || operator === 'notIn') { + if(rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) { + rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1); + } + + return rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context)); + } + + if(operator === 'between') { + const [lhs, rhs] = rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context)); + return [lhs, rhs]; + } + + // gracefully allow both ^[a-Z] as well as /^[a-Z]/ + if(operator === 'matches' || operator === 'notMatches') { + if(rhsOperand.startsWith('/') && rhsOperand.endsWith('/')) { + rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1); + } + + return rhsOperand; + } + + return evaluateJsTemplateLiteral(rhsOperand, context); +}; + class AssertRuntime { runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath) { const enabledAssertions = _.filter(assertions, (a) => a.enabled); @@ -103,7 +154,7 @@ class AssertRuntime { try { const lhs = evaluateJsExpression(lhsExpr, context); - const rhs = evaluateJsExpression(rhsOperand, context); + const rhs = evaluateRhsOperand(rhsOperand, operator, context); switch(operator) { case 'eq': @@ -191,6 +242,7 @@ class AssertRuntime { } assertionResults.push({ + uid: nanoid(), lhsExpr, rhsExpr, rhsOperand, @@ -200,6 +252,7 @@ class AssertRuntime { } catch (err) { assertionResults.push({ + uid: nanoid(), lhsExpr, rhsExpr, rhsOperand,