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';
case 'neq':
return 'notEquals';
case 'gt':
return 'greaterThan';
case 'gte':
return 'greaterThanOrEqual';
case 'lt':
return 'lessThan';
case 'lte':
return 'lessThanOrEqual';
default:
return operator;
}

View File

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

View File

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

View File

@ -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 (
<div className="px-3">
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 (
<StyledWrapper className='flex flex-col px-3'>
<div className="py-2 font-medium test-summary">
@ -39,6 +44,31 @@ const TestResults = ({ results }) => {
</li>
))}
</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>
);
};

View File

@ -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 (
<div className='flex items-center'>
<div>Tests</div>
{numberOfFailedTests ? (
{totalNumberOfFailedTests ? (
<sup className='sups some-tests-failed ml-1 font-medium'>
{numberOfFailedTests}
{totalNumberOfFailedTests}
</sup>
) : (
<sup className='sups all-tests-passed ml-1 font-medium'>
{numberOfTests}
{totalNumberOfTests}
</sup>
)}
</div>

View File

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

View File

@ -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}) {
<ul className="pl-8">
{item.testResults ? item.testResults.map((result) => (
<li key={result.uid} className="py-1">
<li key={result.uid}>
{result.status === 'pass' ? (
<span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
@ -158,6 +170,26 @@ export default function RunnerResults({collection}) {
)}
</li>
)): 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>
</div>
</div>

View File

@ -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]);
};

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) => {
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,

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
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();

View File

@ -1,8 +1,9 @@
const _ = require('lodash');
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,