feat: assertion operator in UI

This commit is contained in:
Anoop M D 2023-02-22 01:20:07 +05:30
parent 224b8c3cc4
commit 34a2e23dc6
6 changed files with 259 additions and 60 deletions

View File

@ -0,0 +1,76 @@
import React from 'react';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const AssertionOperator = ({ operator, onChange }) => {
const operators = [
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'length', 'matches', 'notMatches',
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
const handleChange = (e) => {
onChange(e.target.value);
};
const getLabel = (operator) => {
switch(operator) {
case 'eq':
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;
}
};
return (
<select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => (
<option key={operator} value={operator}>
{getLabel(operator)}
</option>
))}
</select>
);
};
export default AssertionOperator;

View File

@ -0,0 +1,164 @@
import React from 'react';
import { IconTrash } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import AssertionOperator from '../AssertionOperator';
import { useTheme } from 'providers/Theme';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const parseAssertionOperator = (str = '') => {
if(!str || typeof str !== 'string' || !str.length) {
return {
operator: 'eq',
value: str
};
}
const operators = [
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'length', 'matches', 'notMatches',
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
'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,
value
};
}
return {
operator: 'eq',
value: str
};
};
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
return unaryOperators.includes(operator);
};
const AssertionRow = ({
item, collection, assertion, handleAssertionChange, handleRemoveAssertion,
onSave, handleRun
}) => {
const { storedTheme } = useTheme();
const {
operator,
value
} = parseAssertionOperator(assertion.value);
console.log(operator);
console.log(value);
return (
<tr key={assertion.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<td>
<AssertionOperator
operator={operator}
onChange={(op) => handleAssertionChange({
target: {
value: `${op} ${value}`
}
}, assertion, 'value')}
/>
</td>
<td>
{!isUnaryOperator(operator) ? (
<SingleLineEditor
value={value}
theme={storedTheme}
readOnly={true}
onSave={onSave}
onChange={(newValue) => handleAssertionChange({
target: {
value: newValue
}
}, assertion, 'value')}
onRun={handleRun}
collection={collection}
/>
) : (
<input
type="text"
className='cursor-default'
disabled
/>
)}
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={assertion.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
/>
<button onClick={() => handleRemoveAssertion(assertion)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
};
export default AssertionRow;

View File

@ -24,7 +24,7 @@ const Wrapper = styled.div`
width: 30%;
}
&:nth-child(3) {
&:nth-child(4) {
width: 70px;
}
}

View File

@ -1,17 +1,14 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import AssertionRow from './AssertionRow';
import StyledWrapper from './StyledWrapper';
const Assertions = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
const handleAddAssertion = () => {
@ -66,55 +63,25 @@ const Assertions = ({ item, collection }) => {
<thead>
<tr>
<td>Expr</td>
<td>Operator</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{assertions && assertions.length
? assertions.map((assertion, index) => {
? assertions.map((assertion) => {
return (
<tr key={assertion.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={assertion.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleAssertionChange({
target: {
value: newValue
}
}, assertion, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={assertion.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
/>
<button onClick={() => handleRemoveAssertion(assertion)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
<AssertionRow
key={assertion.uid}
assertion={assertion}
item={item}
collection={collection}
handleAssertionChange={handleAssertionChange}
handleRemoveAssertion={handleRemoveAssertion}
onSave={onSave}
handleRun={handleRun}
/>
);
})
: null}

View File

@ -8,7 +8,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Vars from 'components/RequestPane/Vars';
import Assert from 'components/RequestPane/Assert';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
@ -42,7 +42,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
return <Vars item={item} collection={collection} />;
}
case 'assert': {
return <Assert item={item} collection={collection} />;
return <Assertions item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;

View File

@ -11,7 +11,6 @@ const { expect } = chai;
*
* eq : equal to
* neq : not equal to
* like : like
* gt : greater than
* gte : greater than or equal to
* lt : less than
@ -20,7 +19,6 @@ const { expect } = chai;
* notIn : not in
* contains : contains
* notContains : not contains
* count : count
* length : length
* matches : matches
* notMatches : not matches
@ -47,8 +45,8 @@ const parseAssertionOperator = (str = '') => {
}
const operators = [
'eq', 'neq', 'like', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'count', 'length', 'matches', 'notMatches',
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'length', 'matches', 'notMatches',
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
@ -114,9 +112,6 @@ class AssertRuntime {
case 'neq':
expect(lhs).to.not.equal(rhs);
break;
case 'like':
expect(lhs).to.match(new RegExp(rhs));
break;
case 'gt':
expect(lhs).to.be.greaterThan(rhs);
break;
@ -141,9 +136,6 @@ class AssertRuntime {
case 'notContains':
expect(lhs).to.not.include(rhs);
break;
case 'count':
expect(lhs).to.have.lengthOf(rhs);
break;
case 'length':
expect(lhs).to.have.lengthOf(rhs);
break;
@ -160,7 +152,7 @@ class AssertRuntime {
expect(lhs).to.endWith(rhs);
break;
case 'between':
const [min, max] = value.split(' ');
const [min, max] = value.split(',');
expect(lhs).to.be.within(min, max);
break;
case 'isEmpty':