mirror of
https://github.com/usebruno/bruno.git
synced 2025-01-22 13:48:41 +01:00
feat: assertion operator in UI
This commit is contained in:
parent
224b8c3cc4
commit
34a2e23dc6
@ -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;
|
@ -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;
|
@ -24,7 +24,7 @@ const Wrapper = styled.div`
|
|||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(3) {
|
&:nth-child(4) {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,17 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { IconTrash } from '@tabler/icons';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useTheme } from 'providers/Theme';
|
|
||||||
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
|
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
|
||||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import SingleLineEditor from 'components/SingleLineEditor';
|
import AssertionRow from './AssertionRow';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const Assertions = ({ item, collection }) => {
|
const Assertions = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { storedTheme } = useTheme();
|
|
||||||
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
||||||
|
|
||||||
const handleAddAssertion = () => {
|
const handleAddAssertion = () => {
|
||||||
@ -66,55 +63,25 @@ const Assertions = ({ item, collection }) => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Expr</td>
|
<td>Expr</td>
|
||||||
|
<td>Operator</td>
|
||||||
<td>Value</td>
|
<td>Value</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{assertions && assertions.length
|
{assertions && assertions.length
|
||||||
? assertions.map((assertion, index) => {
|
? assertions.map((assertion) => {
|
||||||
return (
|
return (
|
||||||
<tr key={assertion.uid}>
|
<AssertionRow
|
||||||
<td>
|
key={assertion.uid}
|
||||||
<input
|
assertion={assertion}
|
||||||
type="text"
|
item={item}
|
||||||
autoComplete="off"
|
collection={collection}
|
||||||
autoCorrect="off"
|
handleAssertionChange={handleAssertionChange}
|
||||||
autoCapitalize="off"
|
handleRemoveAssertion={handleRemoveAssertion}
|
||||||
spellCheck="false"
|
onSave={onSave}
|
||||||
value={assertion.name}
|
handleRun={handleRun}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
@ -8,7 +8,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
|||||||
import RequestBody from 'components/RequestPane/RequestBody';
|
import RequestBody from 'components/RequestPane/RequestBody';
|
||||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||||
import Vars from 'components/RequestPane/Vars';
|
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 Script from 'components/RequestPane/Script';
|
||||||
import Tests from 'components/RequestPane/Tests';
|
import Tests from 'components/RequestPane/Tests';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -42,7 +42,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
|||||||
return <Vars item={item} collection={collection} />;
|
return <Vars item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
case 'assert': {
|
case 'assert': {
|
||||||
return <Assert item={item} collection={collection} />;
|
return <Assertions item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
case 'script': {
|
case 'script': {
|
||||||
return <Script item={item} collection={collection} />;
|
return <Script item={item} collection={collection} />;
|
||||||
|
@ -11,7 +11,6 @@ const { expect } = chai;
|
|||||||
*
|
*
|
||||||
* eq : equal to
|
* eq : equal to
|
||||||
* neq : not equal to
|
* neq : not equal to
|
||||||
* like : like
|
|
||||||
* gt : greater than
|
* gt : greater than
|
||||||
* gte : greater than or equal to
|
* gte : greater than or equal to
|
||||||
* lt : less than
|
* lt : less than
|
||||||
@ -20,7 +19,6 @@ const { expect } = chai;
|
|||||||
* notIn : not in
|
* notIn : not in
|
||||||
* contains : contains
|
* contains : contains
|
||||||
* notContains : not contains
|
* notContains : not contains
|
||||||
* count : count
|
|
||||||
* length : length
|
* length : length
|
||||||
* matches : matches
|
* matches : matches
|
||||||
* notMatches : not matches
|
* notMatches : not matches
|
||||||
@ -47,8 +45,8 @@ const parseAssertionOperator = (str = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const operators = [
|
const operators = [
|
||||||
'eq', 'neq', 'like', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
|
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
|
||||||
'contains', 'notContains', 'count', 'length', 'matches', 'notMatches',
|
'contains', 'notContains', 'length', 'matches', 'notMatches',
|
||||||
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
|
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
|
||||||
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
|
||||||
];
|
];
|
||||||
@ -114,9 +112,6 @@ class AssertRuntime {
|
|||||||
case 'neq':
|
case 'neq':
|
||||||
expect(lhs).to.not.equal(rhs);
|
expect(lhs).to.not.equal(rhs);
|
||||||
break;
|
break;
|
||||||
case 'like':
|
|
||||||
expect(lhs).to.match(new RegExp(rhs));
|
|
||||||
break;
|
|
||||||
case 'gt':
|
case 'gt':
|
||||||
expect(lhs).to.be.greaterThan(rhs);
|
expect(lhs).to.be.greaterThan(rhs);
|
||||||
break;
|
break;
|
||||||
@ -141,9 +136,6 @@ class AssertRuntime {
|
|||||||
case 'notContains':
|
case 'notContains':
|
||||||
expect(lhs).to.not.include(rhs);
|
expect(lhs).to.not.include(rhs);
|
||||||
break;
|
break;
|
||||||
case 'count':
|
|
||||||
expect(lhs).to.have.lengthOf(rhs);
|
|
||||||
break;
|
|
||||||
case 'length':
|
case 'length':
|
||||||
expect(lhs).to.have.lengthOf(rhs);
|
expect(lhs).to.have.lengthOf(rhs);
|
||||||
break;
|
break;
|
||||||
@ -160,7 +152,7 @@ class AssertRuntime {
|
|||||||
expect(lhs).to.endWith(rhs);
|
expect(lhs).to.endWith(rhs);
|
||||||
break;
|
break;
|
||||||
case 'between':
|
case 'between':
|
||||||
const [min, max] = value.split(' ');
|
const [min, max] = value.split(',');
|
||||||
expect(lhs).to.be.within(min, max);
|
expect(lhs).to.be.within(min, max);
|
||||||
break;
|
break;
|
||||||
case 'isEmpty':
|
case 'isEmpty':
|
||||||
|
Loading…
Reference in New Issue
Block a user