feat: integrated assert runtime for ui

This commit is contained in:
Anoop M D 2023-02-22 02:25:02 +05:30
parent 34a2e23dc6
commit 8044286b80
11 changed files with 204 additions and 31 deletions

View File

@ -49,14 +49,6 @@ const AssertionOperator = ({ operator, onChange }) => {
return 'equals'; return 'equals';
case 'neq': case 'neq':
return 'notEquals'; return 'notEquals';
case 'gt':
return 'greaterThan';
case 'gte':
return 'greaterThanOrEqual';
case 'lt':
return 'lessThan';
case 'lte':
return 'lessThanOrEqual';
default: default:
return operator; return operator;
} }

View File

@ -94,8 +94,6 @@ const AssertionRow = ({
operator, operator,
value value
} = parseAssertionOperator(assertion.value); } = parseAssertionOperator(assertion.value);
console.log(operator);
console.log(value);
return ( return (
<tr key={assertion.uid}> <tr key={assertion.uid}>

View File

@ -7,10 +7,10 @@ const StyledWrapper = styled.div`
.test-failure { .test-failure {
color: ${(props) => props.theme.colors.text.danger}; color: ${(props) => props.theme.colors.text.danger};
}
.error-message { .error-message {
color: ${(props) => props.theme.colors.text.muted}; color: ${(props) => props.theme.colors.text.muted};
}
} }
`; `;

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const TestResults = ({ results }) => { const TestResults = ({ results, assertionResults }) => {
if (!results || !results.length) { results = results || [];
assertionResults = assertionResults || [];
if (!results.length && !assertionResults.length) {
return ( return (
<div className="px-3"> <div className="px-3">
No tests found No tests found
@ -13,6 +15,9 @@ const TestResults = ({ results }) => {
const passedTests = results.filter((result) => result.status === 'pass'); const passedTests = results.filter((result) => result.status === 'pass');
const failedTests = results.filter((result) => result.status === 'fail'); 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 ( return (
<StyledWrapper className='flex flex-col px-3'> <StyledWrapper className='flex flex-col px-3'>
<div className="py-2 font-medium test-summary"> <div className="py-2 font-medium test-summary">
@ -39,6 +44,31 @@ const TestResults = ({ results }) => {
</li> </li>
))} ))}
</ul> </ul>
<div className="py-2 font-medium test-summary">
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed: {failedAssertions.length}
</div>
<ul className="">
{assertionResults.map((result) => (
<li key={result.uid} className="py-1">
{result.status === 'pass' ? (
<span className="test-success">
&#x2714;&nbsp; {result.lhsExpr}: {result.rhsExpr}
</span>
) : (
<>
<span className="test-failure">
&#x2718;&nbsp; {result.lhsExpr}: {result.rhsExpr}
</span>
<br />
<span className="error-message pl-8">
{result.error}
</span>
</>
)}
</li>
))}
</ul>
</StyledWrapper> </StyledWrapper>
); );
}; };

View File

@ -1,23 +1,31 @@
import React from 'react'; import React from 'react';
const TestResultsLabel = ({ results }) => { const TestResultsLabel = ({ results, assertionResults }) => {
if(!results || !results.length) { results = results || [];
assertionResults = assertionResults || [];
if(!results.length && !assertionResults.length) {
return 'Tests'; return 'Tests';
} }
const numberOfTests = results.length; const numberOfTests = results.length;
const numberOfFailedTests = results.filter(result => result.status === 'fail').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 ( return (
<div className='flex items-center'> <div className='flex items-center'>
<div>Tests</div> <div>Tests</div>
{numberOfFailedTests ? ( {totalNumberOfFailedTests ? (
<sup className='sups some-tests-failed ml-1 font-medium'> <sup className='sups some-tests-failed ml-1 font-medium'>
{numberOfFailedTests} {totalNumberOfFailedTests}
</sup> </sup>
) : ( ) : (
<sup className='sups all-tests-passed ml-1 font-medium'> <sup className='sups all-tests-passed ml-1 font-medium'>
{numberOfTests} {totalNumberOfTests}
</sup> </sup>
)} )}
</div> </div>

View File

@ -50,7 +50,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
return <Timeline request={item.requestSent} response={item.response}/>; return <Timeline request={item.requestSent} response={item.response}/>;
} }
case 'tests': { case 'tests': {
return <TestResults results={item.testResults} />; return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
} }
default: { default: {
@ -103,7 +103,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
Timeline Timeline
</div> </div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}> <div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
<TestResultsLabel results={item.testResults} /> <TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
</div> </div>
{!isLoading ? ( {!isLoading ? (
<div className="flex flex-grow justify-end items-center"> <div className="flex flex-grow justify-end items-center">

View File

@ -50,6 +50,14 @@ export default function RunnerResults({collection}) {
} else { } else {
item.testStatus = 'pass'; 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 totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass'); const passedRequests = items.filter((item) => {
const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail'); 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) { if(!items || !items.length) {
return ( return (
@ -139,7 +151,7 @@ export default function RunnerResults({collection}) {
<ul className="pl-8"> <ul className="pl-8">
{item.testResults ? item.testResults.map((result) => ( {item.testResults ? item.testResults.map((result) => (
<li key={result.uid} className="py-1"> <li key={result.uid}>
{result.status === 'pass' ? ( {result.status === 'pass' ? (
<span className="test-success flex items-center"> <span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2"/> <IconCheck size={18} strokeWidth={2} className="mr-2"/>
@ -158,6 +170,26 @@ export default function RunnerResults({collection}) {
)} )}
</li> </li>
)): null} )): null}
{item.assertionResults ? item.assertionResults.map((result) => (
<li key={result.uid}>
{result.status === 'pass' ? (
<span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
{result.lhsExpr}: {result.rhsExpr}
</span>
) : (
<>
<span className="test-failure flex items-center">
<IconX size={18} strokeWidth={2} className="mr-2"/>
{result.lhsExpr}: {result.rhsExpr}
</span>
<span className="error-message pl-8 text-xs">
{result.error}
</span>
</>
)}
</li>
)): null}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ import {
requestSentEvent, requestSentEvent,
requestQueuedEvent, requestQueuedEvent,
testResultsEvent, testResultsEvent,
assertionResultsEvent,
scriptEnvironmentUpdateEvent, scriptEnvironmentUpdateEvent,
collectionRenamedEvent, collectionRenamedEvent,
runFolderEvent runFolderEvent
@ -111,6 +112,10 @@ const useCollectionTreeSync = () => {
dispatch(testResultsEvent(val)); dispatch(testResultsEvent(val));
}; };
const _assertionResults = (val) => {
dispatch(assertionResultsEvent(val));
};
const _collectionRenamed = (val) => { const _collectionRenamed = (val) => {
dispatch(collectionRenamedEvent(val)); dispatch(collectionRenamedEvent(val));
}; };
@ -129,8 +134,9 @@ const useCollectionTreeSync = () => {
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate); const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued); const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
const removeListener8 = ipcRenderer.on('main:test-results', _testResults); const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
const removeListener9 = ipcRenderer.on('main:collection-renamed', _collectionRenamed); const removeListener9 = ipcRenderer.on('main:assertion-results', _assertionResults);
const removeListener10 = ipcRenderer.on('main:run-folder-event', _runFolderEvent); const removeListener10 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
const removeListener11 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
return () => { return () => {
removeListener1(); removeListener1();
@ -143,6 +149,7 @@ const useCollectionTreeSync = () => {
removeListener8(); removeListener8();
removeListener9(); removeListener9();
removeListener10(); removeListener10();
removeListener11();
}; };
}, [isElectron]); }, [isElectron]);
}; };

View File

@ -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) => { collectionRenamedEvent: (state, action) => {
const { collectionPathname, newName } = action.payload; const { collectionPathname, newName } = action.payload;
const collection = findCollectionByPathname(state.collections, collectionPathname); const collection = findCollectionByPathname(state.collections, collectionPathname);
@ -1112,6 +1125,11 @@ export const collectionsSlice = createSlice({
item.testResults = action.payload.testResults; 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') { if(type === 'error') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid); const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
item.error = action.payload.error; item.error = action.payload.error;
@ -1187,6 +1205,7 @@ export const {
collectionUnlinkDirectoryEvent, collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent, collectionAddEnvFileEvent,
testResultsEvent, testResultsEvent,
assertionResultsEvent,
collectionRenamedEvent, collectionRenamedEvent,
toggleRunnerView, toggleRunnerView,
showRunnerView, showRunnerView,

View File

@ -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 // run tests
const testFile = get(item, 'request.tests'); const testFile = get(item, 'request.tests');
if(testFile && testFile.length) { if(testFile && testFile.length) {
@ -359,7 +372,13 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
const postResponseVars = get(request, 'vars.res', []); const postResponseVars = get(request, 'vars.res', []);
if(postResponseVars && postResponseVars.length) { if(postResponseVars && postResponseVars.length) {
const varsRuntime = new VarsRuntime(); 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 // 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'); const testFile = get(item, 'request.tests');
if(testFile && testFile.length) { if(testFile && testFile.length) {
const testRuntime = new TestRuntime(); const testRuntime = new TestRuntime();

View File

@ -1,8 +1,9 @@
const _ = require('lodash'); const _ = require('lodash');
const chai = require('chai'); const chai = require('chai');
const { nanoid } = require('nanoid');
const Bru = require('../bru'); const Bru = require('../bru');
const BrunoRequest = require('../bruno-request'); const BrunoRequest = require('../bruno-request');
const { evaluateJsExpression, createResponseParser } = require('../utils'); const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { expect } = chai; const { expect } = chai;
@ -51,9 +52,20 @@ const parseAssertionOperator = (str = '') => {
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean' '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 [operator, ...rest] = str.trim().split(' ');
const value = rest.join(' '); const value = rest.join(' ');
if(unaryOperators.includes(operator)) {
return {
operator,
value: ''
};
}
if(operators.includes(operator)) { if(operators.includes(operator)) {
return { return {
operator, 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 { class AssertRuntime {
runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath) { runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath) {
const enabledAssertions = _.filter(assertions, (a) => a.enabled); const enabledAssertions = _.filter(assertions, (a) => a.enabled);
@ -103,7 +154,7 @@ class AssertRuntime {
try { try {
const lhs = evaluateJsExpression(lhsExpr, context); const lhs = evaluateJsExpression(lhsExpr, context);
const rhs = evaluateJsExpression(rhsOperand, context); const rhs = evaluateRhsOperand(rhsOperand, operator, context);
switch(operator) { switch(operator) {
case 'eq': case 'eq':
@ -191,6 +242,7 @@ class AssertRuntime {
} }
assertionResults.push({ assertionResults.push({
uid: nanoid(),
lhsExpr, lhsExpr,
rhsExpr, rhsExpr,
rhsOperand, rhsOperand,
@ -200,6 +252,7 @@ class AssertRuntime {
} }
catch (err) { catch (err) {
assertionResults.push({ assertionResults.push({
uid: nanoid(),
lhsExpr, lhsExpr,
rhsExpr, rhsExpr,
rhsOperand, rhsOperand,