mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
52 Commits
v0.9.4
...
feature/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fc32d035f | ||
|
|
78251c530c | ||
|
|
dea95664b9 | ||
|
|
fbc6e7bff5 | ||
|
|
4884106aaa | ||
|
|
5c15438949 | ||
|
|
b53a9eaee9 | ||
|
|
5899ca446d | ||
|
|
d21e7f6fb5 | ||
|
|
ee8a3eae8c | ||
|
|
fac5109242 | ||
|
|
47dfbd2a64 | ||
|
|
074d72d885 | ||
|
|
8c29d131e2 | ||
|
|
437044bdcd | ||
|
|
2120a562da | ||
|
|
04c3c2dbf1 | ||
|
|
1d03e1d5ea | ||
|
|
2b174e1c60 | ||
|
|
7a2b32069e | ||
|
|
a9e6c3a35c | ||
|
|
e6a754b933 | ||
|
|
ee4509f037 | ||
|
|
c04f0e7a71 | ||
|
|
2f52ce4c71 | ||
|
|
b1edaba1c6 | ||
|
|
3f6fcdd582 | ||
|
|
c745786b1c | ||
|
|
9e30c7b440 | ||
|
|
b87cc7ccae | ||
|
|
1595d736f2 | ||
|
|
b38c25ca70 | ||
|
|
f22858219b | ||
|
|
8044286b80 | ||
|
|
34a2e23dc6 | ||
|
|
224b8c3cc4 | ||
|
|
d58e92205b | ||
|
|
925af1f26f | ||
|
|
d07744d5c2 | ||
|
|
5efb18ad63 | ||
|
|
9cfb54ee9f | ||
|
|
4c9d22d1e0 | ||
|
|
c5d43cc9e6 | ||
|
|
8300830a95 | ||
|
|
2dfc972930 | ||
|
|
4fdfdaf2cb | ||
|
|
a1385ba1e2 | ||
|
|
15804ac293 | ||
|
|
0244b2e1d6 | ||
|
|
9d3762702f | ||
|
|
209f30998e | ||
|
|
e777eed00d |
4
.github/workflows/unit-tests.yml
vendored
4
.github/workflows/unit-tests.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
run: npm run build --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
|
||||
@@ -23,25 +23,7 @@ You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) an
|
||||
|
||||
### Lets start coding
|
||||
|
||||
```bash
|
||||
# clone and cd into bruno
|
||||
# use Node 14.x, Npm 8.x
|
||||
|
||||
# Install deps (note that we use npm workspaces)
|
||||
npm i
|
||||
|
||||
# run next app
|
||||
npm run dev:web
|
||||
|
||||
# run electron app
|
||||
# neededonly if you want to test changes related to electron app
|
||||
# please note that both web and electron use the same code
|
||||
# if it works in web, then it should also work in electron
|
||||
npm run dev:electron
|
||||
|
||||
# open in browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
|
||||
|
||||
### Raising Pull Request
|
||||
|
||||
|
||||
@@ -15,19 +15,33 @@ nvm use
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev --workspace=packages/bruno-app
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev --workspace=packages/bruno-electron
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
@@ -35,6 +49,5 @@ You might encounter a `Unsupported platform` error when you run `npm install`. T
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
@@ -14,17 +15,19 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"jest": "^29.2.0",
|
||||
"randomstring": "^1.2.2"
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
@@ -32,4 +35,4 @@
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -50,7 +50,6 @@
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"yup": "^0.32.11"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
@@ -8,6 +8,12 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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';
|
||||
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,162 @@
|
||||
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);
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-assertion {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import AssertionRow from './AssertionRow';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Assertions = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
||||
|
||||
const handleAddAssertion = () => {
|
||||
dispatch(
|
||||
addAssertion({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleAssertionChange = (e, _assertion, type) => {
|
||||
const assertion = cloneDeep(_assertion);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
assertion.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
assertion.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
assertion.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateAssertion({
|
||||
assertion: assertion,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveAssertion = (assertion) => {
|
||||
dispatch(
|
||||
deleteAssertion({
|
||||
assertUid: assertion.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Expr</td>
|
||||
<td>Operator</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assertions && assertions.length
|
||||
? assertions.map((assertion) => {
|
||||
return (
|
||||
<AssertionRow
|
||||
key={assertion.uid}
|
||||
assertion={assertion}
|
||||
item={item}
|
||||
collection={collection}
|
||||
handleAssertionChange={handleAssertionChange}
|
||||
handleRemoveAssertion={handleRemoveAssertion}
|
||||
onSave={onSave}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
|
||||
+ Add Assertion
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Assertions;
|
||||
@@ -8,6 +8,8 @@ import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -91,6 +93,12 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
@@ -130,9 +138,15 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
@@ -143,7 +157,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
) : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||
<span className='ml-1'>Schema</span>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center cursor-pointer hover:underline ml-2'
|
||||
|
||||
@@ -32,7 +32,7 @@ const useGraphqlSchema = (endpoint, environment) => {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('Graphql Schema loaded successfully');
|
||||
toast.success('GraphQL Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
@@ -40,7 +40,7 @@ const useGraphqlSchema = (endpoint, environment) => {
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading Graphql Schema');
|
||||
toast.error('Error occured while loading GraphQL Schema');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
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 Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -36,6 +38,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
@@ -67,7 +75,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Params
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
@@ -75,9 +83,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
@@ -89,7 +103,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className={`flex w-full ${focusedTab.requestPaneTab === 'script' ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
@@ -52,7 +52,7 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
onModeChange('formUrlEncoded');
|
||||
}}
|
||||
>
|
||||
Form Url Encoded
|
||||
Form URL Encoded
|
||||
</div>
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
|
||||
@@ -65,7 +65,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
@@ -6,8 +6,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: rgb(155 155 155);
|
||||
font-weight: 500;
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ const Script = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1'>
|
||||
<div className='mb-1 title'>Request</div>
|
||||
<div className='flex-1 mt-2'>
|
||||
<div className='mb-1 title text-xs'>Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
@@ -53,7 +53,7 @@ const Script = ({ item, collection }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 mt-6'>
|
||||
<div className='mt-1 mb-1 title'>Response</div>
|
||||
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const handleAddVar = () => {
|
||||
dispatch(
|
||||
addVar({
|
||||
type: varType,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
_var.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{ varType === 'request' ? (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var"/>
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var"/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var, index) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleVarChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, _var, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
||||
56
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
56
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Vars = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1 mt-2'>
|
||||
<div className='mb-1 title text-xs'>Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType='request'/>
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType='response'/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
||||
@@ -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};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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">
|
||||
✔ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -55,12 +55,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
};
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
@@ -80,6 +74,12 @@ const Collection = ({ collection, searchText }) => {
|
||||
})
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
@@ -84,7 +84,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
@@ -21,6 +22,14 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
|
||||
const handleImportInsomniaCollection = () => {
|
||||
importInsomniaCollection()
|
||||
.then((collection) => {
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
@@ -36,6 +45,12 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
>
|
||||
Postman Collection
|
||||
</div>
|
||||
<div
|
||||
className='text-link hover:underline cursor-pointer mt-2'
|
||||
onClick={handleImportInsomniaCollection}
|
||||
>
|
||||
Insomnia Collection
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
Http
|
||||
HTTP
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -118,7 +118,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
Graphql
|
||||
GraphQL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
Url
|
||||
URL
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2 ">
|
||||
|
||||
@@ -117,7 +117,7 @@ const Sidebar = () => {
|
||||
</GitHubButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.9.4</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.10.2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,8 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-line {
|
||||
color: ${(props) => props.theme.text};
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setValue(this.props.value || '');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone } from '@tabler/icons';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
@@ -74,15 +74,18 @@ const Welcome = () => {
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
{/* <div className="flex items-center mt-2">
|
||||
<IconBook size={18} strokeWidth={2}/><span className="label ml-2">Docs</span>
|
||||
</div> */}
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
|
||||
@@ -144,6 +145,23 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [setShowNewRequestModal]);
|
||||
|
||||
// close tab hotkey
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [activeTabUid]
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+w', 'ctrl+w']);
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
||||
|
||||
@@ -8,7 +8,7 @@ import filter from 'lodash/filter';
|
||||
import each from 'lodash/each';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import splitOnFirst from 'split-on-first';
|
||||
import { splitOnFirst } from 'utils/url';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
findCollectionByPathname,
|
||||
@@ -183,7 +183,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
scriptEnvironmentUpdateEvent: (state, action) => {
|
||||
const { collectionUid, environment, collectionVariables } = action.payload;
|
||||
const { collectionUid, envVariables, collectionVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
@@ -191,7 +191,7 @@ export const collectionsSlice = createSlice({
|
||||
const activeEnvironment = findEnvironmentInCollection(collection, activeEnvironmentUid);
|
||||
|
||||
if (activeEnvironment) {
|
||||
forOwn(environment, (value, key) => {
|
||||
forOwn(envVariables, (value, key) => {
|
||||
const variable = find(activeEnvironment.variables, (v) => v.name === key);
|
||||
|
||||
if (variable) {
|
||||
@@ -201,7 +201,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
collection.collectionVariables = collectionVariables;
|
||||
|
||||
}
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
@@ -707,6 +706,153 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
addAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.assertions = item.draft.request.assertions || [];
|
||||
item.draft.request.assertions.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
updateAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const assertion = item.draft.request.assertions.find((a) => a.uid === action.payload.assertion.uid);
|
||||
if (assertion) {
|
||||
assertion.name = action.payload.assertion.name;
|
||||
assertion.value = action.payload.assertion.value;
|
||||
assertion.enabled = action.payload.assertion.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteAssertion: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.assertions = item.draft.request.assertions.filter((a) => a.uid !== action.payload.assertUid);
|
||||
}
|
||||
}
|
||||
},
|
||||
addVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
item.draft.request.vars.req.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
item.draft.request.vars.res.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
|
||||
const reqVar = find(item.draft.request.vars.req, (v) => v.uid === action.payload.var.uid);
|
||||
if (reqVar) {
|
||||
reqVar.name = action.payload.var.name;
|
||||
reqVar.value = action.payload.var.value;
|
||||
reqVar.description = action.payload.var.description;
|
||||
reqVar.enabled = action.payload.var.enabled;
|
||||
}
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
const resVar = find(item.draft.request.vars.res, (v) => v.uid === action.payload.var.uid);
|
||||
if (resVar) {
|
||||
resVar.name = action.payload.var.name;
|
||||
resVar.value = action.payload.var.value;
|
||||
resVar.description = action.payload.var.description;
|
||||
resVar.enabled = action.payload.var.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
if(type === 'request') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
item.draft.request.vars.req = item.draft.request.vars.req.filter((v) => v.uid !== action.payload.varUid);
|
||||
} else if(type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
item.draft.request.vars.res = item.draft.request.vars.res || [];
|
||||
item.draft.request.vars.res = item.draft.request.vars.res.filter((v) => v.uid !== action.payload.varUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
@@ -876,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);
|
||||
@@ -966,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;
|
||||
@@ -1028,6 +1192,12 @@ export const {
|
||||
updateResponseScript,
|
||||
updateRequestTests,
|
||||
updateRequestMethod,
|
||||
addAssertion,
|
||||
updateAssertion,
|
||||
deleteAssertion,
|
||||
addVar,
|
||||
updateVar,
|
||||
deleteVar,
|
||||
collectionAddFileEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionChangeFileEvent,
|
||||
@@ -1035,6 +1205,7 @@ export const {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
collectionRenamedEvent,
|
||||
toggleRunnerView,
|
||||
showRunnerView,
|
||||
|
||||
@@ -9,6 +9,9 @@ const deleteUidsInItems = (items) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
each(get(item, 'request.headers'), (header) => delete header.uid);
|
||||
each(get(item, 'request.params'), (param) => delete param.uid);
|
||||
each(get(item, 'request.vars.req'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.res'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
|
||||
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
|
||||
}
|
||||
|
||||
@@ -282,6 +282,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||
},
|
||||
script: si.draft.request.script,
|
||||
vars: si.draft.request.vars,
|
||||
assertions: si.draft.request.assertions,
|
||||
tests: si.draft.request.tests
|
||||
};
|
||||
}
|
||||
@@ -302,6 +304,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
},
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
tests: si.request.tests
|
||||
};
|
||||
}
|
||||
@@ -349,6 +353,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
headers: [],
|
||||
body: _item.request.body,
|
||||
script: _item.request.script,
|
||||
vars: _item.request.vars,
|
||||
assertions: _item.request.assertions,
|
||||
tests: _item.request.tests
|
||||
}
|
||||
};
|
||||
@@ -427,7 +433,7 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
label = 'Form Url Encoded';
|
||||
label = 'Form URL Encoded';
|
||||
break;
|
||||
}
|
||||
case 'multipartForm': {
|
||||
|
||||
@@ -32,6 +32,9 @@ export const updateUidsInCollection = (_collection) => {
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.query'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
|
||||
|
||||
186
packages/bruno-app/src/utils/importers/insomnia-collection.js
Normal file
186
packages/bruno-app/src/utils/importers/insomnia-collection.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
try {
|
||||
const graphql = JSON.parse(text);
|
||||
|
||||
return {
|
||||
query: graphql.query,
|
||||
variables: JSON.stringify(graphql.variables, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const transformInsomniaRequestItem = (request) => {
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: request.name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(request.headers, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.parameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '');
|
||||
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = request.body.text;
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = request.body.text;
|
||||
} else if (mimeType === 'text/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = request.body.text;
|
||||
} else if (mimeType === 'application/graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQL(request.body.text);
|
||||
}
|
||||
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const parseInsomniaCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: "1",
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const insomniaExport = JSON.parse(data);
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find(resource => resource._type === 'workspace' && resource.scope === 'collection');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||
}
|
||||
|
||||
brunoCollection.name = insomniaCollection.name;
|
||||
|
||||
const requestsAndFolders = insomniaResources.filter(
|
||||
(resource) => resource._type === 'request' || resource._type === 'request_group'
|
||||
) || [];
|
||||
|
||||
function createFolderStructure(resources, parentId = null) {
|
||||
const requestGroups = resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder) => {
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === folder._id);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: folder.name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem)),
|
||||
}
|
||||
});
|
||||
|
||||
return folders.concat(requests.map(transformInsomniaRequestItem));
|
||||
}
|
||||
|
||||
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id),
|
||||
|
||||
resolve(brunoCollection);
|
||||
} catch (err) {
|
||||
reject(new BrunoError('An error occurred while parsing the Insomnia collection'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseInsomniaCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
@@ -40,3 +40,16 @@ export const stringifyQueryParams = (params) => {
|
||||
|
||||
return queryString.join('&');
|
||||
};
|
||||
|
||||
export const splitOnFirst = (str, char) => {
|
||||
if(!str || !str.length) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
let index = str.indexOf(char);
|
||||
if (index === -1) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseQueryParams } from './index';
|
||||
import { parseQueryParams, splitOnFirst } from './index';
|
||||
|
||||
describe('Url Utils - parseQueryParams', () => {
|
||||
it('should parse query - case 1', () => {
|
||||
@@ -41,3 +41,30 @@ describe('Url Utils - parseQueryParams', () => {
|
||||
expect(params).toEqual([{name: 'a', value: '1'}, {name: 'b', value: '2'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - splitOnFirst', () => {
|
||||
it('should split on first - case 1', () => {
|
||||
const params = splitOnFirst("a", "=");
|
||||
expect(params).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should split on first - case 2', () => {
|
||||
const params = splitOnFirst("a=", "=");
|
||||
expect(params).toEqual(['a', '']);
|
||||
});
|
||||
|
||||
it('should split on first - case 3', () => {
|
||||
const params = splitOnFirst("a=1", "=");
|
||||
expect(params).toEqual(['a', '1']);
|
||||
});
|
||||
|
||||
it('should split on first - case 4', () => {
|
||||
const params = splitOnFirst("a=1&b=2", "=");
|
||||
expect(params).toEqual(['a', '1&b=2']);
|
||||
});
|
||||
|
||||
it('should split on first - case 5', () => {
|
||||
const params = splitOnFirst("a=1&b=2", "&");
|
||||
expect(params).toEqual(['a=1', 'b=2']);
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 953 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,52 +0,0 @@
|
||||
let currentTab = {
|
||||
id: null,
|
||||
url: null,
|
||||
};
|
||||
|
||||
const getExtensionId = () => {
|
||||
const matches = chrome.runtime.getURL('x').match(/.*\/\/(.*)\/x$/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
return chrome.runtime.id;
|
||||
};
|
||||
|
||||
// Create a new tab for the extension
|
||||
function createNewTab() {
|
||||
chrome.tabs.create({ url: 'index.html' }, function (tab) {
|
||||
currentTab = {
|
||||
id: tab.id,
|
||||
url: tab.url
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Focus on the open extension tab
|
||||
function focusTab(tabId) {
|
||||
var updateProperties = { "active": true };
|
||||
chrome.tabs.update(tabId, updateProperties, function (tab) { });
|
||||
}
|
||||
|
||||
// Open the extension tab when the extension icon is clicked
|
||||
chrome.action.onClicked.addListener(function (tab) {
|
||||
if (!currentTab || !currentTab.id) {
|
||||
createNewTab();
|
||||
} else {
|
||||
chrome.tabs.get(currentTab.id, function (tab) {
|
||||
console.log(chrome.runtime.id, tab.url);
|
||||
if (tab && tab.url && tab.url.includes(getExtensionId())) {
|
||||
focusTab(currentTab.id);
|
||||
} else {
|
||||
createNewTab();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// When a tab is closed, check if it is the extension tab that was closed, and unset currentTabId
|
||||
chrome.tabs.onRemoved.addListener(function (tabId) {
|
||||
if (tabId === currentTab.id) {
|
||||
currentTab = {};
|
||||
}
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"version": "0.1.0",
|
||||
"name": "Bruno API Client",
|
||||
"short_name": "Bruno",
|
||||
"description": "Opensource API Client",
|
||||
"icons": {
|
||||
"16": "assets/images/logo-16x16.png",
|
||||
"48": "assets/images/logo-48x48.png",
|
||||
"128": "assets/images/logo-128x128.png"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "js/background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_icon": "assets/images/logo-128x128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://*/",
|
||||
"https://*/"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "0.2.3",
|
||||
"version": "0.3.0",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"bru": "./bin/bru.js"
|
||||
@@ -11,7 +11,7 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.1.1",
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"axios": "^1.3.2",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -60,7 +60,7 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assert');
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath);
|
||||
|
||||
@@ -42,7 +42,7 @@ const bruToJson = (bru) => {
|
||||
"headers": _.get(json, "headers", []),
|
||||
"body": _.get(json, "body", {}),
|
||||
"vars": _.get(json, "vars", []),
|
||||
"assert": _.get(json, "assert", []),
|
||||
"assertions": _.get(json, "assertions", []),
|
||||
"script": _.get(json, "script", ""),
|
||||
"tests": _.get(json, "tests", "")
|
||||
}
|
||||
|
||||
4
packages/bruno-electron/.gitignore
vendored
4
packages/bruno-electron/.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
node_modules
|
||||
web
|
||||
out
|
||||
.env
|
||||
|
||||
// certs
|
||||
sectigo.*
|
||||
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
|
||||
@@ -6,6 +6,7 @@ directories:
|
||||
output: out
|
||||
files:
|
||||
- "**/*"
|
||||
afterSign: notarize.js
|
||||
mac:
|
||||
artifactName: ${name}_${version}_${arch}_${os}.${ext}
|
||||
category: public.app-category.developer-tools
|
||||
@@ -20,6 +21,7 @@ mac:
|
||||
- arm64
|
||||
icon: resources/icons/mac/icon.icns
|
||||
hardenedRuntime: true
|
||||
identity: "Anoop MD (W7LPPWA48L)"
|
||||
entitlements: resources/entitlements.mac.plist
|
||||
entitlementsInherit: resources/entitlements.mac.plist
|
||||
linux:
|
||||
@@ -31,3 +33,5 @@ linux:
|
||||
win:
|
||||
artifactName: ${name}_${version}_${arch}_win.${ext}
|
||||
icon: resources/icons/png
|
||||
certificateFile: sectigo.pfx
|
||||
certificatePassword: "secret"
|
||||
|
||||
36
packages/bruno-electron/notarize.js
Normal file
36
packages/bruno-electron/notarize.js
Normal file
@@ -0,0 +1,36 @@
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const electron_notarize = require('electron-notarize');
|
||||
|
||||
const notarize = async function (params) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
let appId = 'com.usebruno.app';
|
||||
|
||||
let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
|
||||
if (!fs.existsSync(appPath)) {
|
||||
console.error(`Cannot find application at: ${appPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Notarizing ${appId} found at ${appPath} using Apple ID ${process.env.APPLE_ID}`);
|
||||
|
||||
try {
|
||||
await electron_notarize.notarize({
|
||||
appBundleId: appId,
|
||||
appPath: appPath,
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD,
|
||||
ascProvider: 'W7LPPWA48L'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log(`Done notarizing ${appId}`);
|
||||
};
|
||||
|
||||
module.exports = notarize;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.9.4",
|
||||
"version": "0.10.2",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -9,17 +9,19 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "electron .",
|
||||
"dist": "electron-builder --mac --win --linux",
|
||||
"pack-app": "electron-builder --dir"
|
||||
"dist": "electron-builder --win --linux --mac",
|
||||
"pack": "electron-builder --dir"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.1.0",
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"@usebruno/schema": "0.1.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-util": "^0.17.2",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -3,4 +3,7 @@
|
||||
```bash
|
||||
# electron dev
|
||||
npm start
|
||||
|
||||
# generate pfx file for signing windows build
|
||||
openssl pkcs12 -export -inkey sectigo.key -in sectigo.pem -out sectigo.pfx
|
||||
```
|
||||
|
||||
@@ -40,11 +40,17 @@ const hydrateRequestWithUuid = (request, pathname) => {
|
||||
|
||||
const params = _.get(request, 'request.params', []);
|
||||
const headers = _.get(request, 'request.headers', []);
|
||||
const requestVars = _.get(request, 'request.vars.req', []);
|
||||
const responseVars = _.get(request, 'request.vars.res', []);
|
||||
const assertions = _.get(request, 'request.assertions', []);
|
||||
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
|
||||
|
||||
params.forEach((param) => param.uid = uuid());
|
||||
headers.forEach((header) => header.uid = uuid());
|
||||
requestVars.forEach((variable) => variable.uid = uuid());
|
||||
responseVars.forEach((variable) => variable.uid = uuid());
|
||||
assertions.forEach((assertion) => assertion.uid = uuid());
|
||||
bodyFormUrlEncoded.forEach((param) => param.uid = uuid());
|
||||
bodyMultipartForm.forEach((param) => param.uid = uuid());
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ const bruToJson = (bru) => {
|
||||
"headers": _.get(json, "headers", []),
|
||||
"body": _.get(json, "body", {}),
|
||||
"script": _.get(json, "script", {}),
|
||||
"vars": _.get(json, "vars", {}),
|
||||
"assertions": _.get(json, "assertions", []),
|
||||
"tests": _.get(json, "tests", "")
|
||||
}
|
||||
};
|
||||
@@ -113,6 +115,11 @@ const jsonToBru = (json) => {
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend, each, get } = require('lodash');
|
||||
const { ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
|
||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||
@@ -96,13 +96,27 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
const envVars = getEnvVars(environment);
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if(preRequestVars && preRequestVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const result = varsRuntime.runPreRequestVars(preRequestVars, request, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run pre-request script
|
||||
const requestScript = get(request, 'script.req');
|
||||
if(requestScript && requestScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath);
|
||||
const result = await scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
environment: result.environment,
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
@@ -127,18 +141,44 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
const response = await axios(request);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
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 post-response script
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
environment: result.environment,
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
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) {
|
||||
const testRuntime = new TestRuntime();
|
||||
@@ -284,18 +324,27 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if(preRequestVars && preRequestVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPreRequestVars(preRequestVars, request, envVars, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run pre-request script
|
||||
const requestScript = get(request, 'script.req');
|
||||
if(requestScript && requestScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
environment: result.environment,
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVars, collectionVariables);
|
||||
|
||||
// todo:
|
||||
@@ -312,22 +361,52 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
// send request
|
||||
timeStart = Date.now();
|
||||
const response = await axios(request);
|
||||
timeEnd = Date.now();
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
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
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
environment: result.environment,
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -63,6 +63,9 @@ const prepareRequest = (request) => {
|
||||
axiosRequest.script = request.script;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -13,8 +13,9 @@
|
||||
"test": "jest --testPathIgnorePatterns test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"atob": "^2.1.2",
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"json-query": "^2.2.2",
|
||||
|
||||
@@ -1,17 +1,49 @@
|
||||
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;
|
||||
chai.use(function (chai, utils) {
|
||||
// Custom assertion for checking if a variable is JSON
|
||||
chai.Assertion.addProperty('json', function () {
|
||||
const obj = this._obj;
|
||||
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) && obj.constructor === Object;
|
||||
|
||||
this.assert(
|
||||
isJson,
|
||||
`expected ${utils.inspect(obj)} to be JSON`,
|
||||
`expected ${utils.inspect(obj)} not to be JSON`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Custom assertion for matching regex
|
||||
chai.use(function (chai, utils) {
|
||||
chai.Assertion.addMethod('match', function (regex) {
|
||||
const obj = this._obj;
|
||||
let match = false;
|
||||
if(obj === undefined) {
|
||||
match = false;
|
||||
} else {
|
||||
match = regex.test(obj);
|
||||
}
|
||||
|
||||
this.assert(
|
||||
match,
|
||||
`expected ${utils.inspect(obj)} to match ${regex}`,
|
||||
`expected ${utils.inspect(obj)} not to match ${regex}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
* eq : equal to
|
||||
* neq : not equal to
|
||||
* like : like
|
||||
* gt : greater than
|
||||
* gte : greater than or equal to
|
||||
* lt : less than
|
||||
@@ -20,7 +52,6 @@ const { expect } = chai;
|
||||
* notIn : not in
|
||||
* contains : contains
|
||||
* notContains : not contains
|
||||
* count : count
|
||||
* length : length
|
||||
* matches : matches
|
||||
* notMatches : not matches
|
||||
@@ -47,15 +78,26 @@ 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'
|
||||
];
|
||||
|
||||
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,
|
||||
@@ -69,6 +111,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;
|
||||
}
|
||||
|
||||
// gracefully allow 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);
|
||||
@@ -105,7 +186,7 @@ class AssertRuntime {
|
||||
|
||||
try {
|
||||
const lhs = evaluateJsExpression(lhsExpr, context);
|
||||
const rhs = evaluateJsExpression(rhsOperand, context);
|
||||
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
|
||||
|
||||
switch(operator) {
|
||||
case 'eq':
|
||||
@@ -114,9 +195,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 +219,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 +235,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':
|
||||
@@ -199,6 +274,7 @@ class AssertRuntime {
|
||||
}
|
||||
|
||||
assertionResults.push({
|
||||
uid: nanoid(),
|
||||
lhsExpr,
|
||||
rhsExpr,
|
||||
rhsOperand,
|
||||
@@ -208,6 +284,7 @@ class AssertRuntime {
|
||||
}
|
||||
catch (err) {
|
||||
assertionResults.push({
|
||||
uid: nanoid(),
|
||||
lhsExpr,
|
||||
rhsExpr,
|
||||
rhsOperand,
|
||||
@@ -222,4 +299,4 @@ class AssertRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AssertRuntime;
|
||||
module.exports = AssertRuntime;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const { NodeVM } = require('vm2');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const stream = require('stream');
|
||||
const util = require('util');
|
||||
const zlib = require('zlib');
|
||||
const url = require('url');
|
||||
const punycode = require('punycode');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const BrunoResponse = require('../bruno-response');
|
||||
@@ -11,14 +18,15 @@ const lodash = require('lodash');
|
||||
const moment = require('moment');
|
||||
const uuid = require('uuid');
|
||||
const nanoid = require('nanoid');
|
||||
const axios = require('axios');
|
||||
const CryptoJS = require('crypto-js');
|
||||
|
||||
class ScriptRuntime {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
runRequestScript(script, request, environment, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(environment, collectionVariables);
|
||||
async runRequestScript(script, request, envVariables, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const context = {
|
||||
@@ -32,28 +40,46 @@ class ScriptRuntime {
|
||||
external: true,
|
||||
root: [collectionPath],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
stream,
|
||||
util,
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
punycode,
|
||||
zlib,
|
||||
// 3rd party libs
|
||||
atob,
|
||||
btoa,
|
||||
lodash,
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
'crypto-js': CryptoJS
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vm.run(script, path.join(collectionPath, 'vm.js'));
|
||||
// wrap script inside a async function that gets called
|
||||
script = `return (async () => { ${script} })()`;
|
||||
|
||||
// bug that needs to be fixed
|
||||
// vm.run is not awaiting the async function
|
||||
// created an issue in vm2 repo: https://github.com/patriksimek/vm2/issues/513
|
||||
const result = await vm.run(script, path.join(collectionPath, 'vm.js'));
|
||||
console.log(result);
|
||||
|
||||
return {
|
||||
request,
|
||||
environment,
|
||||
envVariables,
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
|
||||
runResponseScript(script, request, response, environment, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(environment, collectionVariables);
|
||||
runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
@@ -84,7 +110,7 @@ class ScriptRuntime {
|
||||
|
||||
return {
|
||||
response,
|
||||
environment,
|
||||
envVariables,
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class TestRuntime {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
runTests(testsFile, request, response, environment, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(environment, collectionVariables);
|
||||
runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestRuntime {
|
||||
|
||||
return {
|
||||
request,
|
||||
environment,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
results: __brunoTestResults.getResults()
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
|
||||
class VarsRuntime {
|
||||
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) {
|
||||
@@ -25,9 +25,13 @@ class VarsRuntime {
|
||||
}
|
||||
|
||||
_.each(enabledVars, (v) => {
|
||||
const value = evaluateJsExpression(v.value, context);
|
||||
const value = evaluateJsTemplateLiteral(v.value, context);
|
||||
bru.setVar(v.name, value);
|
||||
});
|
||||
|
||||
return {
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
|
||||
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
@@ -56,6 +60,11 @@ class VarsRuntime {
|
||||
const value = evaluateJsExpression(v.value, context);
|
||||
bru.setVar(v.name, value);
|
||||
});
|
||||
|
||||
return {
|
||||
envVariables,
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const jsonQuery = require('json-query');
|
||||
const { get } = require("@usebruno/query");
|
||||
|
||||
const JS_KEYWORDS = `
|
||||
break case catch class const continue debugger default delete do
|
||||
@@ -59,10 +60,49 @@ const evaluateJsExpression = (expression, context) => {
|
||||
return fn(context);
|
||||
};
|
||||
|
||||
const evaluateJsTemplateLiteral = (templateLiteral, context) => {
|
||||
if(!templateLiteral || !templateLiteral.length || typeof templateLiteral !== 'string') {
|
||||
return templateLiteral;
|
||||
}
|
||||
|
||||
templateLiteral = templateLiteral.trim();
|
||||
|
||||
if(templateLiteral === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(templateLiteral === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(templateLiteral === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(templateLiteral === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
|
||||
return templateLiteral.slice(1, -1);
|
||||
}
|
||||
|
||||
if(templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
|
||||
return templateLiteral.slice(1, -1);
|
||||
}
|
||||
|
||||
if(!isNaN(templateLiteral)) {
|
||||
return Number(templateLiteral);
|
||||
}
|
||||
|
||||
templateLiteral = "`" + templateLiteral + "`";
|
||||
|
||||
return evaluateJsExpression(templateLiteral, context);
|
||||
};
|
||||
|
||||
const createResponseParser = (response = {}) => {
|
||||
const res = (expr) => {
|
||||
const output = jsonQuery(expr, { data: response.data });
|
||||
return output ? output.value : null;
|
||||
const res = (expr, ...fns) => {
|
||||
return get(response.data, expr, ...fns);
|
||||
};
|
||||
|
||||
res.status = response.status;
|
||||
@@ -70,11 +110,17 @@ const createResponseParser = (response = {}) => {
|
||||
res.headers = response.headers;
|
||||
res.body = response.data;
|
||||
|
||||
res.jq = (expr) => {
|
||||
const output = jsonQuery(expr, { data: response.data });
|
||||
return output ? output.value : null;
|
||||
};
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
evaluateJsExpression,
|
||||
evaluateJsTemplateLiteral,
|
||||
createResponseParser,
|
||||
internalExpressionCache
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const { evaluateJsExpression, internalExpressionCache: cache } = require("../src/utils");
|
||||
const { describe, it, expect } = require("@jest/globals");
|
||||
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require("../src/utils");
|
||||
|
||||
describe("utils", () => {
|
||||
describe("expression evaluation", () => {
|
||||
@@ -112,4 +113,28 @@ describe("utils", () => {
|
||||
expect(result).toBe(startTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe("response parser", () => {
|
||||
const res = createResponseParser({
|
||||
status: 200,
|
||||
data: {
|
||||
order: {
|
||||
items: [
|
||||
{ id: 1, amount: 10 },
|
||||
{ id: 2, amount: 20 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should default to bruno query", () => {
|
||||
const value = res("..items[?].amount[0]", i => i.amount > 10);
|
||||
expect(value).toBe(20);
|
||||
});
|
||||
|
||||
it("should allow json-query", () => {
|
||||
const value = res.jq("order.items[amount > 10].amount");
|
||||
expect(value).toBe(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,7 +381,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
},
|
||||
assert(_1, dictionary) {
|
||||
return {
|
||||
assert: mapPairListToKeyValPairs(dictionary.ast)
|
||||
assertions: mapPairListToKeyValPairs(dictionary.ast)
|
||||
};
|
||||
},
|
||||
scriptreq(_1, _2, _3, _4, textblock, _5) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const jsonToBru = (json) => {
|
||||
script,
|
||||
tests,
|
||||
vars,
|
||||
assert,
|
||||
assertions,
|
||||
docs
|
||||
} = json;
|
||||
|
||||
@@ -196,15 +196,15 @@ ${indentString(body.xml)}
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if(assert && assert.length) {
|
||||
if(assertions && assertions.length) {
|
||||
bru += `assert {`;
|
||||
|
||||
if(enabled(assert).length) {
|
||||
bru += `\n${indentString(enabled(assert).map(item => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
if(enabled(assertions).length) {
|
||||
bru += `\n${indentString(enabled(assertions).map(item => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if(disabled(assert).length) {
|
||||
bru += `\n${indentString(disabled(assert).map(item => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
if(disabled(assertions).length) {
|
||||
bru += `\n${indentString(disabled(assertions).map(item => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
|
||||
@@ -13,7 +13,7 @@ assert {
|
||||
|
||||
const output = parser(input);
|
||||
const expected = {
|
||||
"assert": [{
|
||||
"assertions": [{
|
||||
name: "res(\"data.airports\").filter(a => a.code ===\"BLR\").name",
|
||||
value: '"Bangalore International Airport"',
|
||||
enabled: true
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"assert": [
|
||||
"assertions": [
|
||||
{
|
||||
"name": "$res.status",
|
||||
"value": "200",
|
||||
|
||||
22
packages/bruno-query/.gitignore
vendored
Normal file
22
packages/bruno-query/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# production
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
5
packages/bruno-query/jest.config.js
Normal file
5
packages/bruno-query/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
32
packages/bruno-query/package.json
Normal file
32
packages/bruno-query/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@usebruno/query",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "rollup -c",
|
||||
"prepack": "npm run test && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
33
packages/bruno-query/readme.md
Normal file
33
packages/bruno-query/readme.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# bruno-query
|
||||
|
||||
Bruno query with deep navigation, filter and map support
|
||||
|
||||
Easy array navigation
|
||||
```js
|
||||
get(data, 'customer.orders.items.amount')
|
||||
```
|
||||
Deep navigation .. double dots
|
||||
```js
|
||||
get(data, '..items.amount')
|
||||
```
|
||||
Array indexing
|
||||
```js
|
||||
get(data, '..items[0].amount')
|
||||
```
|
||||
Array filtering [?] with corresponding filter function
|
||||
```js
|
||||
get(data, '..items[?].amount', i => i.amount > 20)
|
||||
```
|
||||
Array filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)
|
||||
```js
|
||||
get(data, '..items[?]', { id: 2, amount: 20 })
|
||||
```
|
||||
Array mapping [?] with corresponding mapper function
|
||||
```js
|
||||
get(data, '..items[?].amount', i => i.amount + 10)
|
||||
```
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
40
packages/bruno-query/rollup.config.js
Normal file
40
packages/bruno-query/rollup.config.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { nodeResolve } = require("@rollup/plugin-node-resolve");
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
const typescript = require("@rollup/plugin-typescript");
|
||||
const dts = require("rollup-plugin-dts");
|
||||
const { terser } = require("rollup-plugin-terser");
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: "src/index.ts",
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: "cjs",
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: "esm",
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.css']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: "./tsconfig.json" }),
|
||||
terser()
|
||||
]
|
||||
},
|
||||
{
|
||||
input: "dist/esm/index.d.ts",
|
||||
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
||||
plugins: [dts.default()],
|
||||
}
|
||||
];
|
||||
151
packages/bruno-query/src/index.ts
Normal file
151
packages/bruno-query/src/index.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* If value is an array returns the deeply flattened array, otherwise value
|
||||
*/
|
||||
function normalize(value: any) {
|
||||
if (!Array.isArray(value)) return value;
|
||||
|
||||
const values = [] as any[];
|
||||
|
||||
value.forEach(item => {
|
||||
const value = normalize(item);
|
||||
if (value != null) {
|
||||
values.push(...Array.isArray(value) ? value : [value]);
|
||||
}
|
||||
});
|
||||
|
||||
return values.length ? values : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value of a prop from source.
|
||||
*
|
||||
* If source is an array get value from each item.
|
||||
*
|
||||
* If deep is true then recursively gets values for prop in nested objects.
|
||||
*
|
||||
* Once a value is found will not recurse further into that value.
|
||||
*/
|
||||
function getValue(source: any, prop: string, deep = false): any {
|
||||
if (typeof source !== 'object') return;
|
||||
|
||||
let value;
|
||||
|
||||
if (Array.isArray(source)) {
|
||||
value = source.map(item => getValue(item, prop, deep));
|
||||
} else {
|
||||
value = source[prop];
|
||||
if (deep) {
|
||||
value = [value];
|
||||
for (const [key, item] of Object.entries(source)) {
|
||||
if (key !== prop && typeof item === 'object') {
|
||||
value.push(getValue(source[key], prop, deep));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalize(value);
|
||||
}
|
||||
|
||||
type PredicateOrMapper = ((obj: any) => any) | Record<string, any>;
|
||||
|
||||
/**
|
||||
* Make a predicate function that checks scalar properties for equality
|
||||
*/
|
||||
function objectPredicate(obj: Record<string, any>) {
|
||||
return (item: any) => {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (item[key] !== value) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter on source array or object
|
||||
*
|
||||
* If the filter returns a non boolean non null value it is treated as a mapped value
|
||||
*/
|
||||
function filterOrMap(source: any, funOrObj: PredicateOrMapper) {
|
||||
const fun = typeof funOrObj === 'object' ? objectPredicate(funOrObj) : funOrObj;
|
||||
const isArray = Array.isArray(source);
|
||||
const list = isArray ? source : [source];
|
||||
const result = [] as any[];
|
||||
for (const item of list) {
|
||||
if (item == null) continue;
|
||||
const value = fun(item);
|
||||
if (value === true) {
|
||||
result.push(item); // predicate
|
||||
} else if (value != null && value !== false) {
|
||||
result.push(value); // mapper
|
||||
}
|
||||
}
|
||||
return normalize(isArray ? result : result[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter with deep navigation, filter and map support
|
||||
*
|
||||
* 1. Easy array navigation
|
||||
* ```js
|
||||
* get(data, 'customer.orders.items.amount')
|
||||
* ```
|
||||
* 2. Deep navigation .. double dots
|
||||
* ```js
|
||||
* get(data, '..items.amount')
|
||||
* ```
|
||||
* 3. Array indexing
|
||||
* ```js
|
||||
* get(data, '..items[0].amount')
|
||||
* ```
|
||||
* 4. Array filtering [?] with corresponding filter function
|
||||
* ```js
|
||||
* get(data, '..items[?].amount', i => i.amount > 20)
|
||||
* ```
|
||||
* 5. Array filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)
|
||||
* ```js
|
||||
* get(data, '..items[?]', { id: 2, amount: 20 })
|
||||
* ```
|
||||
* 6. Array mapping [?] with corresponding mapper function
|
||||
* ```js
|
||||
* get(data, '..items[?].amount', i => i.amount + 10)
|
||||
* ```
|
||||
*/
|
||||
export function get(source: any, path: string, ...fns: PredicateOrMapper[]) {
|
||||
const paths = path
|
||||
.replace(/\s+/g, '')
|
||||
.split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // ["..", "items", "[?]", ".", "amount", "[0]" ]
|
||||
.filter(s => s.length > 0)
|
||||
.map(str => {
|
||||
str = str.replace(/\[|\]/g, '');
|
||||
const index = parseInt(str);
|
||||
return isNaN(index) ? str : index;
|
||||
});
|
||||
|
||||
let index = 0, lookbehind = '' as string | number, funIndex = 0;
|
||||
|
||||
while (source != null && index < paths.length) {
|
||||
const token = paths[index++];
|
||||
|
||||
switch (true) {
|
||||
case token === "..":
|
||||
case token === ".":
|
||||
break;
|
||||
case token === "?":
|
||||
const fun = fns[funIndex++];
|
||||
if (fun == null)
|
||||
throw new Error(`missing function for ${lookbehind}`);
|
||||
source = filterOrMap(source, fun);
|
||||
break;
|
||||
case typeof token === 'number':
|
||||
source = normalize(source[token]);
|
||||
break;
|
||||
default:
|
||||
source = getValue(source, token as string, lookbehind === "..");
|
||||
}
|
||||
|
||||
lookbehind = token;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
61
packages/bruno-query/tests/index.spec.ts
Normal file
61
packages/bruno-query/tests/index.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
|
||||
import { get } from '../src/index';
|
||||
|
||||
const data = {
|
||||
customer: {
|
||||
address: {
|
||||
city: "bangalore"
|
||||
},
|
||||
orders: [
|
||||
{
|
||||
id: "order-1",
|
||||
items: [
|
||||
{ id: 1, amount: 10 },
|
||||
{ id: 2, amount: 20 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "order-2",
|
||||
items: [
|
||||
{ id: 3, amount: 30, },
|
||||
{ id: 4, amount: 40 }
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe("get", () => {
|
||||
it.each([
|
||||
["customer.address.city", "bangalore"],
|
||||
["customer.orders.items.amount", [10, 20, 30, 40]],
|
||||
["customer.orders.items.amount[0]", 10],
|
||||
["..items.amount", [10, 20, 30, 40]],
|
||||
["..amount", [10, 20, 30, 40]],
|
||||
["..items.amount[0]", 10],
|
||||
["..items[0].amount", 10],
|
||||
["..items[5].amount", undefined], // invalid index
|
||||
["..id", ["order-1", 1, 2, "order-2", 3, 4]], // all ids
|
||||
["customer.orders.foo", undefined],
|
||||
["..customer.foo", undefined],
|
||||
["..address", [{ city: "bangalore" }]], // .. will return array
|
||||
["..address[0]", { city: "bangalore" }],
|
||||
])("%s should be %j", (expr, result) => {
|
||||
expect(get(data, expr)).toEqual(result);
|
||||
});
|
||||
|
||||
// filter and map
|
||||
it.each([
|
||||
["..items[?].amount", [40], (i: any) => i.amount > 30], // [?] filter
|
||||
["..items[?].amount", [40], { id: 4, amount: 40 }], // object filter
|
||||
["..items[?].amount", undefined, { id: 5, amount: 40 }],
|
||||
["..items..amount[?][0]", 40, (amt: number) => amt > 30],
|
||||
["..items..amount[0][?]", undefined, (amt: number) => amt > 30], // filter on single value
|
||||
["..items..amount[?]", [11, 21, 31, 41], (amt: number) => amt + 1], // [?] mapper
|
||||
["..items..amount[0][?]", 11, (amt: number) => amt + 1], // [?] map on single value
|
||||
])("%s should be %j for %s", (expr, result, filter) => {
|
||||
expect(get(data, expr, filter)).toEqual(result);
|
||||
});
|
||||
|
||||
});
|
||||
23
packages/bruno-query/tsconfig.json
Normal file
23
packages/bruno-query/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"declarationDir": "types",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"tests"
|
||||
],
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -26,6 +26,15 @@ const keyValueSchema = Yup.object({
|
||||
enabled: Yup.boolean()
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
const varsSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
description: Yup.string().nullable(),
|
||||
local: Yup.boolean(),
|
||||
enabled: Yup.boolean()
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
const requestUrlSchema = Yup.string().min(0).defined();
|
||||
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).required('method is required');
|
||||
|
||||
@@ -57,6 +66,11 @@ const requestSchema = Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
}).noUnknown(true).strict(),
|
||||
vars: Yup.object({
|
||||
req: Yup.array().of(varsSchema).nullable(),
|
||||
res: Yup.array().of(varsSchema).nullable()
|
||||
}).noUnknown(true).strict().nullable(),
|
||||
assertions: Yup.array().of(keyValueSchema).nullable(),
|
||||
tests: Yup.string().nullable()
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Remove any chrome-extension directory
|
||||
rm -rf chrome-extension
|
||||
|
||||
# Remove any bruno.zip files
|
||||
rm bruno.zip
|
||||
|
||||
# Create a new chrome-extension directory
|
||||
mkdir chrome-extension
|
||||
|
||||
# Copy build
|
||||
cp -r packages/bruno-app/out/* chrome-extension
|
||||
|
||||
# Copy the chrome extension files
|
||||
cp -r packages/bruno-chrome-extension/* chrome-extension
|
||||
|
||||
# Filenames starting with "_" are reserved for use by the system
|
||||
mv chrome-extension/_next chrome-extension/next
|
||||
sed -i'' -e 's@/_next/@/next/@g' chrome-extension/**.html
|
||||
|
||||
# Remove sourcemaps
|
||||
find chrome-extension -name '*.map' -type f -delete
|
||||
|
||||
# Compress the chrome-extension directory into a zip file
|
||||
zip -r bruno.zip chrome-extension
|
||||
Reference in New Issue
Block a user