mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
96 Commits
v0.9.0
...
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 | ||
|
|
7e70d05dc8 | ||
|
|
e60b06e4a4 | ||
|
|
17ded5de4c | ||
|
|
e1b97643bd | ||
|
|
8103554545 | ||
|
|
a425b42615 | ||
|
|
b14f867811 | ||
|
|
013abeaa80 | ||
|
|
9d3762702f | ||
|
|
cac9f9aef4 | ||
|
|
2b63368f2c | ||
|
|
acd980ffc6 | ||
|
|
1a175e4449 | ||
|
|
209f30998e | ||
|
|
e777eed00d | ||
|
|
15fc24679c | ||
|
|
48d26c05d9 | ||
|
|
9d395ded33 | ||
|
|
943e74c327 | ||
|
|
b852d1cc52 | ||
|
|
3d22f77226 | ||
|
|
429ca4093c | ||
|
|
a4f757ee87 | ||
|
|
df4f322024 | ||
|
|
ddd39e630d | ||
|
|
ef8e8bf637 | ||
|
|
7405fa9709 | ||
|
|
242fcac2d3 | ||
|
|
efd15838aa | ||
|
|
c55f9d42da | ||
|
|
2f32f7024e | ||
|
|
aff6499478 | ||
|
|
45ed47ff90 | ||
|
|
27c6c1349a | ||
|
|
c78ffa3a80 | ||
|
|
6b2d335ade | ||
|
|
837e39d870 | ||
|
|
67643c4c48 | ||
|
|
2b384656b6 | ||
|
|
411c06f4cb | ||
|
|
03fa46d8b3 | ||
|
|
d0f2eb27bc | ||
|
|
1b9ec05a58 | ||
|
|
3f74178c81 | ||
|
|
78ca6c5e96 | ||
|
|
5f59a16090 | ||
|
|
3805cef0c4 |
6
.github/workflows/unit-tests.yml
vendored
6
.github/workflows/unit-tests.yml
vendored
@@ -15,9 +15,15 @@ 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
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,40 +1,53 @@
|
||||
## development
|
||||
## Development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
###
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# run next app
|
||||
npm run dev --workspace=packages/bruno-app
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# run electron app
|
||||
npm run dev --workspace=packages/bruno-electron
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# build next app
|
||||
npm run build --workspace=packages/bruno-app
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### fix
|
||||
### 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.
|
||||
|
||||
### testing
|
||||
```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
|
||||
# bruno-schema
|
||||
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} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -9,14 +10,36 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
if(selectedEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if(environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [collection, environments]);
|
||||
}, [collection, environments, selectedEnvironment]);
|
||||
|
||||
useEffect(() => {
|
||||
// check env add
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
|
||||
if(newEnv){
|
||||
setSelectedEnvironment(newEnv);
|
||||
}
|
||||
}
|
||||
|
||||
// check env delete
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
@@ -31,7 +54,11 @@ const EnvironmentList = ({ collection }) => {
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div key={env.uid} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} onClick={() => setSelectedEnvironment(env)}>
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => setSelectedEnvironment(env)}
|
||||
>
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import slash from 'utils/common/slash';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
// convert to unix style path
|
||||
fullPath = slash(fullPath);
|
||||
pathname = slash(pathname);
|
||||
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir, name } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
}
|
||||
|
||||
export default function RunnerResults({collection}) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,6 +32,7 @@ export default function RunnerResults({collection}) {
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
@@ -40,11 +50,61 @@ 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';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass');
|
||||
const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail');
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
};
|
||||
|
||||
const closeRunner = () => {
|
||||
dispatch(closeCollectionRunner({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
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 (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
You have <span className='font-medium'>{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
@@ -91,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"/>
|
||||
@@ -110,11 +170,45 @@ 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>
|
||||
);
|
||||
})}
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='flex flex-1' style={{width: '50%'}}>
|
||||
{selectedItem ? (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -19,8 +20,13 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occured while cloning the request')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -15,12 +15,24 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
folderName: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
folderName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
folderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'folderName',
|
||||
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
|
||||
test:(value) => {
|
||||
if(item && item.uid) {
|
||||
return true;
|
||||
}
|
||||
return value && !(value.trim().toLowerCase().includes('environments'))
|
||||
}
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
|
||||
.then(() => onClose())
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,14 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
requestMethod: 'GET'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
requestName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
requestName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'requestName',
|
||||
message: 'The request name "index" is reserved in bruno',
|
||||
test: value => value && !(value.trim().toLowerCase().includes('index')),
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (isEphermal) {
|
||||
@@ -49,7 +56,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
);
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
} else {
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
@@ -62,7 +69,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
.catch(() => toast.error('An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -95,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
|
||||
@@ -111,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>
|
||||
@@ -138,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.0</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} />
|
||||
|
||||
@@ -43,7 +43,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
.btn-close {
|
||||
color: ${(props) => props.theme.button.close.color};
|
||||
background: ${(props) => props.theme.button.close.bg};
|
||||
border: solid 1px ${(props) => props.theme.button.close.border};;
|
||||
border: solid 1px ${(props) => props.theme.button.close.border};
|
||||
|
||||
&.btn-border {
|
||||
border: solid 1px #696969;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
13
packages/bruno-app/src/hooks/usePrevious/index.js
Normal file
13
packages/bruno-app/src/hooks/usePrevious/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value; //assign the value of ref to the argument
|
||||
},[value]); //this code will run when the value of 'value' changes
|
||||
|
||||
return ref.current; //in the end, return the current ref value.
|
||||
}
|
||||
|
||||
export default usePrevious;
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
requestSentEvent,
|
||||
requestQueuedEvent,
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
collectionRenamedEvent,
|
||||
runFolderEvent
|
||||
@@ -33,6 +34,10 @@ const useCollectionTreeSync = () => {
|
||||
};
|
||||
|
||||
const _collectionTreeUpdated = (type, val) => {
|
||||
if(window.__IS_DEV__) {
|
||||
console.log(type);
|
||||
console.log(val);
|
||||
}
|
||||
if (type === 'addDir') {
|
||||
dispatch(
|
||||
collectionAddDirectoryEvent({
|
||||
@@ -107,6 +112,10 @@ const useCollectionTreeSync = () => {
|
||||
dispatch(testResultsEvent(val));
|
||||
};
|
||||
|
||||
const _assertionResults = (val) => {
|
||||
dispatch(assertionResultsEvent(val));
|
||||
};
|
||||
|
||||
const _collectionRenamed = (val) => {
|
||||
dispatch(collectionRenamedEvent(val));
|
||||
};
|
||||
@@ -125,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();
|
||||
@@ -139,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)} />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
import toast from 'react-hot-toast';
|
||||
import trim from 'lodash/trim';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { uuid } from 'utils/common';
|
||||
@@ -17,13 +18,15 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
isItemARequest,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
refreshUidsInItem
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
updateLastAction,
|
||||
resetRunResults,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
@@ -191,7 +194,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('folder with same name already exists'));
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
} else {
|
||||
const currentItem = findItemInCollection(collection, itemUid);
|
||||
@@ -206,7 +209,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('folder with same name already exists'));
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
} else {
|
||||
return reject(new Error('unable to find parent folder'));
|
||||
@@ -230,14 +233,14 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
return reject(new Error('Unable to locate item'));
|
||||
}
|
||||
|
||||
const dirname = path.dirname(item.pathname);
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
|
||||
let newPathname = '';
|
||||
if (item.type === 'folder') {
|
||||
newPathname = `${dirname}${PATH_SEPARATOR}${trim(newName)}`;
|
||||
newPathname = path.join(dirname, trim(newName));
|
||||
} else {
|
||||
const filename = resolveRequestFilename(newName);
|
||||
newPathname = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||
newPathname = path.join(dirname, filename);
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
@@ -284,13 +287,13 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in collection`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
} else {
|
||||
const reqWithSameNameExists = find(parentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||
if (!reqWithSameNameExists) {
|
||||
const dirname = path.dirname(item.pathname);
|
||||
const fullName = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
const fullName = path.join(dirname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? (requestItems.length + 1) : 1;
|
||||
@@ -301,7 +304,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in the folder`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -543,7 +546,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in collection`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
} else {
|
||||
const currentItem = findItemInCollection(collection, itemUid);
|
||||
@@ -557,7 +560,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
} else {
|
||||
return reject(new Error(`${requestName} already exists in the folder`));
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,6 +577,13 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name)
|
||||
.then(dispatch(updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
}
|
||||
})))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
areItemsTheSameExceptSeqUpdate
|
||||
} from 'utils/collections';
|
||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platform';
|
||||
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
@@ -38,6 +38,14 @@ export const collectionsSlice = createSlice({
|
||||
createCollection: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
// this is used in scenarios where we want to know the last action performed on the collection
|
||||
// and take some extra action based on that
|
||||
// for example, when a env is created, we want to auto select it the env modal
|
||||
collection.lastAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
@@ -54,13 +62,12 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
addEnvironment: (state, action) => {
|
||||
const { environment, collectionUid } = action.payload;
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.environments = collection.environments || [];
|
||||
collection.environments.push(environment);
|
||||
collection.lastAction = lastAction;
|
||||
}
|
||||
},
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
@@ -176,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) {
|
||||
@@ -184,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) {
|
||||
@@ -194,7 +201,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
collection.collectionVariables = collectionVariables;
|
||||
|
||||
}
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
@@ -700,12 +706,159 @@ 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);
|
||||
|
||||
if (collection) {
|
||||
const dirname = path.dirname(file.meta.pathname);
|
||||
const dirname = getDirectoryName(file.meta.pathname);
|
||||
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||
let currentPath = collection.pathname;
|
||||
let currentSubItems = collection.items;
|
||||
@@ -846,6 +999,14 @@ export const collectionsSlice = createSlice({
|
||||
existingEnv.variables = environment.variables;
|
||||
} else {
|
||||
collection.environments.push(environment);
|
||||
|
||||
const lastAction = collection.lastAction;
|
||||
if(lastAction && lastAction.type === 'ADD_ENVIRONMENT') {
|
||||
collection.lastAction = null;
|
||||
if(lastAction.payload === environment.name) {
|
||||
collection.activeEnvironmentUid = environment.uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -861,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);
|
||||
@@ -873,10 +1047,8 @@ export const collectionsSlice = createSlice({
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
console.log('here');
|
||||
if (collection) {
|
||||
console.log('here2');
|
||||
collection.showRunner = !collection.showRunner;
|
||||
collection.showRunner = !collection.showRunner;
|
||||
}
|
||||
},
|
||||
showRunnerView: (state, action) => {
|
||||
@@ -904,14 +1076,30 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
runFolderEvent: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, type, error } = action.payload;
|
||||
const { collectionUid, folderUid, itemUid, type, isRecursive, error } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
const request = findItemInCollection(collection, itemUid);
|
||||
|
||||
collection.runnerResult = collection.runnerResult || {items: []};
|
||||
collection.runnerResult = collection.runnerResult || {info: {}, items: []};
|
||||
|
||||
// todo
|
||||
// get startedAt and endedAt from the runner and display it in the UI
|
||||
if(type === 'testrun-started') {
|
||||
const info = collection.runnerResult.info;
|
||||
info.collectionUid = collectionUid;
|
||||
info.folderUid = folderUid;
|
||||
info.isRecursive = isRecursive;
|
||||
info.status = 'started';
|
||||
}
|
||||
|
||||
if(type === 'testrun-ended') {
|
||||
const info = collection.runnerResult.info;
|
||||
info.status = 'ended';
|
||||
}
|
||||
|
||||
|
||||
if(type === 'request-queued') {
|
||||
collection.runnerResult.items.push({
|
||||
@@ -937,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;
|
||||
@@ -944,6 +1137,15 @@ export const collectionsSlice = createSlice({
|
||||
item.status = "error";
|
||||
}
|
||||
}
|
||||
},
|
||||
closeCollectionRunner: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.showRunner = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -952,7 +1154,7 @@ export const {
|
||||
createCollection,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
addEnvironment,
|
||||
updateLastAction,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
@@ -990,6 +1192,12 @@ export const {
|
||||
updateResponseScript,
|
||||
updateRequestTests,
|
||||
updateRequestMethod,
|
||||
addAssertion,
|
||||
updateAssertion,
|
||||
deleteAssertion,
|
||||
addVar,
|
||||
updateVar,
|
||||
deleteVar,
|
||||
collectionAddFileEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionChangeFileEvent,
|
||||
@@ -997,12 +1205,14 @@ export const {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
collectionRenamedEvent,
|
||||
toggleRunnerView,
|
||||
showRunnerView,
|
||||
hideRunnerView,
|
||||
resetRunResults,
|
||||
runFolderEvent
|
||||
runFolderEvent,
|
||||
closeCollectionRunner
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -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': {
|
||||
@@ -518,6 +524,19 @@ export const getEnvironmentVariables = (collection) => {
|
||||
return variables;
|
||||
}
|
||||
|
||||
export const getTotalRequestCountInCollection = (collection) => {
|
||||
let count = 0;
|
||||
each(collection.items, (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
count++;
|
||||
} else if (isItemAFolder(item)) {
|
||||
count += getTotalRequestCountInCollection(item);
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@@ -18,9 +19,17 @@ export const resolveRequestFilename = (name) => {
|
||||
};
|
||||
|
||||
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
if (!path.isAbsolute(pathname)) {
|
||||
throw new Error('Invalid path!');
|
||||
}
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
rootPath = slash(rootPath);
|
||||
|
||||
const relativePath = path.relative(rootPath, pathname);
|
||||
return relativePath ? relativePath.split(path.sep) : [];
|
||||
};
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
|
||||
return path.dirname(pathname);
|
||||
}
|
||||
|
||||
20
packages/bruno-app/src/utils/common/slash.js
Normal file
20
packages/bruno-app/src/utils/common/slash.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export default slash;
|
||||
@@ -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()));
|
||||
|
||||
@@ -50,8 +53,6 @@ export const updateUidsInCollection = (_collection) => {
|
||||
};
|
||||
updateEnvUids(collection.environments);
|
||||
|
||||
console.log(collection);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
|
||||
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,21 +1,27 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"bru": "./bin/bru.js"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"bin",
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.1.0",
|
||||
"@usebruno/lang": "0.1.0",
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"axios": "^1.3.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"inquirer": "^9.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
8
packages/bruno-cli/readme.md
Normal file
8
packages/bruno-cli/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# bruno-cli
|
||||
|
||||
Bru CLI
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
@@ -1,43 +1,237 @@
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const {
|
||||
exists,
|
||||
isFile,
|
||||
isDirectory
|
||||
} = require('../utils/filesystem');
|
||||
const {
|
||||
runSingleRequest
|
||||
} = require('../runner/run-single-request');
|
||||
const path = require('path');
|
||||
const { exists, isFile, isDirectory, getSubDirectories } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
const { rpad } = require('../utils/common');
|
||||
const { bruToJson } = require('../utils/bru');
|
||||
|
||||
const command = 'run <filename>';
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
const printRunSummary = (assertionResults, testResults) => {
|
||||
// display assertion results and test results summary
|
||||
const totalAssertions = assertionResults.length;
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass').length;
|
||||
const failedAssertions = totalAssertions - passedAssertions;
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter((result) => result.status === 'pass').length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
const maxLength = 12;
|
||||
|
||||
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
|
||||
if (failedTests > 0) {
|
||||
assertSummary += `, ${chalk.red(`${failedTests} failed`)}`;
|
||||
}
|
||||
assertSummary += `, ${totalTests} total`;
|
||||
|
||||
let testSummary = `${rpad('Assertions:', maxLength)} ${chalk.green(`${passedAssertions} passed`)}`;
|
||||
if (failedAssertions > 0) {
|
||||
testSummary += `, ${chalk.red(`${failedAssertions} failed`)}`;
|
||||
}
|
||||
testSummary += `, ${totalAssertions} total`;
|
||||
|
||||
console.log("\n" + chalk.bold(assertSummary));
|
||||
console.log(chalk.bold(testSummary));
|
||||
};
|
||||
|
||||
const getBruFilesRecursively = (dir) => {
|
||||
const environmentsPath = 'environments';
|
||||
|
||||
const getFilesInOrder = (dir) => {
|
||||
let bruJsons = [];
|
||||
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
// todo: we might need a ignore config inside bruno.json
|
||||
if (stats.isDirectory() &&
|
||||
filePath !== environmentsPath &&
|
||||
!filePath.startsWith(".git") &&
|
||||
!filePath.startsWith("node_modules")
|
||||
) {
|
||||
traverse(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const currentDirBruJsons = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
currentDirBruJsons.push({
|
||||
bruFilepath: filePath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// order requests by sequence
|
||||
currentDirBruJsons.sort((a, b) => {
|
||||
const aSequence = a.bruJson.seq || 0;
|
||||
const bSequence = b.bruJson.seq || 0;
|
||||
return aSequence - bSequence;
|
||||
});
|
||||
|
||||
bruJsons = bruJsons.concat(currentDirBruJsons);
|
||||
};
|
||||
|
||||
traverse(dir);
|
||||
return bruJsons;
|
||||
};
|
||||
|
||||
const bruJsons = getFilesInOrder(dir);
|
||||
return bruJsons;
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
yargs.example('$0 run request.bru', 'Run a request');
|
||||
yargs
|
||||
.option('r', {
|
||||
describe: 'Indicates a recursive run',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
})
|
||||
.option('env', {
|
||||
describe: 'Environment variables',
|
||||
type: 'string',
|
||||
})
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
.example('$0 run folder -r', 'Run all requests in a folder recursively')
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
try {
|
||||
const { filename } = argv;
|
||||
|
||||
const pathExists = await exists(filename);
|
||||
if(!pathExists) {
|
||||
console.error(chalk.red(`File or directory ${filename} does not exist`));
|
||||
}
|
||||
let {
|
||||
filename,
|
||||
env,
|
||||
r: recursive
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
// todo
|
||||
// right now, bru must be run from the root of the collection
|
||||
// will add support in the future to run it from anywhere inside the collection
|
||||
const collectionPath = process.cwd();
|
||||
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
||||
const brunoJsonExists = await exists(brunoJsonPath);
|
||||
if(!brunoJsonExists) {
|
||||
console.error(chalk.red(`You can run only at the root of a collection`));
|
||||
return;
|
||||
}
|
||||
|
||||
if(filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if(!pathExists) {
|
||||
console.error(chalk.red(`File or directory ${filename} does not exist`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
filename = "./";
|
||||
recursive = true;
|
||||
}
|
||||
|
||||
const collectionVariables = {};
|
||||
let envVars = {};
|
||||
|
||||
if(env) {
|
||||
const envFile = path.join(collectionPath, 'environments', `${env}.bru`);
|
||||
const envPathExists = await exists(envFile);
|
||||
|
||||
if(!envPathExists) {
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`));
|
||||
return;
|
||||
}
|
||||
|
||||
const envBruContent = fs.readFileSync(envFile, 'utf8');
|
||||
const envJson = bruToEnvJson(envBruContent);
|
||||
envVars = getEnvVars(envJson);
|
||||
}
|
||||
|
||||
const _isFile = await isFile(filename);
|
||||
if(_isFile) {
|
||||
console.log(chalk.yellow('Running Request \n'));
|
||||
await runSingleRequest(filename, collectionPath, collectionVariables);
|
||||
console.log(chalk.green('\nDone!'));
|
||||
const bruContent = fs.readFileSync(filename, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
const result = await runSingleRequest(filename, bruJson, collectionPath, collectionVariables, envVars);
|
||||
|
||||
if(result) {
|
||||
const {
|
||||
assertionResults,
|
||||
testResults
|
||||
} = result;
|
||||
|
||||
printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Done.')));
|
||||
}
|
||||
}
|
||||
|
||||
const _isDirectory = await isDirectory(filename);
|
||||
if(_isDirectory) {
|
||||
let bruJsons = [];
|
||||
if(!recursive) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
const files = fs.readdirSync(filename);
|
||||
const bruFiles = files.filter((file) => file.endsWith('.bru'));
|
||||
|
||||
for (const bruFile of bruFiles) {
|
||||
const bruFilepath = path.join(filename, bruFile)
|
||||
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
bruJsons.push({
|
||||
bruFilepath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
|
||||
// order requests by sequence
|
||||
bruJsons.sort((a, b) => {
|
||||
const aSequence = a.bruJson.seq || 0;
|
||||
const bSequence = b.bruJson.seq || 0;
|
||||
return aSequence - bSequence;
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.yellow('Running Folder Recursively \n'));
|
||||
|
||||
bruJsons = await getBruFilesRecursively(filename);
|
||||
}
|
||||
|
||||
let assertionResults = [];
|
||||
let testResults = [];
|
||||
|
||||
for (const iter of bruJsons) {
|
||||
const {
|
||||
bruFilepath,
|
||||
bruJson
|
||||
} = iter;
|
||||
const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars);
|
||||
|
||||
if(result) {
|
||||
const {
|
||||
assertionResults: _assertionResults,
|
||||
testResults: _testResults
|
||||
} = result;
|
||||
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
}
|
||||
|
||||
printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.log("Something went wrong");
|
||||
console.error(chalk.red(err.message));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,45 +1,14 @@
|
||||
const Mustache = require('mustache');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const { forOwn, each, extend, get } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime } = require('@usebruno/js');
|
||||
const {
|
||||
bruToJson
|
||||
} = require('./bru');
|
||||
const {
|
||||
stripExtension
|
||||
} = require('../utils/filesystem');
|
||||
const chalk = require('chalk');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if(variable.enabled) {
|
||||
envVars[variable.name] = Mustache.escape(variable.value);
|
||||
}
|
||||
});
|
||||
|
||||
return envVars;
|
||||
};
|
||||
|
||||
const runSingleRequest = async function (filename, collectionPath, collectionVariables) {
|
||||
const runSingleRequest = async function (filename, bruJson, collectionPath, collectionVariables, envVariables) {
|
||||
try {
|
||||
const bruContent = fs.readFileSync(filename, 'utf8');
|
||||
|
||||
const bruJson = bruToJson(bruContent);
|
||||
const request = prepareRequest(bruJson.request);
|
||||
|
||||
// make axios work in node using form data
|
||||
@@ -53,34 +22,68 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
const envVars = getEnvVars({});
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if(preRequestVars && preRequestVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPreRequestVars(preRequestVars, request, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(bruJson, 'request.script.req');
|
||||
if(requestScriptFile && requestScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runRequestScript(requestScriptFile, request, envVars, collectionVariables, collectionPath);
|
||||
scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables);
|
||||
|
||||
// run request
|
||||
const response = await axios(request);
|
||||
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runResponseVars(bruJson.request.vars.res, request, response, envVars, collectionVariables);
|
||||
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`));
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(postResponseVars, request, response, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if(responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScriptFile, response, envVars, collectionVariables, collectionPath);
|
||||
scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if(r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath);
|
||||
const result = testRuntime.runTests(testFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`));
|
||||
if(testResults && testResults.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if(testResult.status === 'pass') {
|
||||
@@ -90,9 +93,13 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
assertionResults,
|
||||
testResults
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err.response);
|
||||
Promise.reject(err);
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
bruToJsonV2
|
||||
} = require('@usebruno/lang');
|
||||
const Mustache = require('mustache');
|
||||
const { bruToEnvJsonV2, bruToJsonV2 } = require('@usebruno/lang');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a BRU file to JSON.
|
||||
@@ -38,13 +42,13 @@ 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", "")
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.body.mode = _.get(json, "http.mode", "none");
|
||||
transformedJson.request.body.mode = _.get(json, "http.body", "none");
|
||||
|
||||
return transformedJson;
|
||||
} catch (err) {
|
||||
@@ -52,6 +56,32 @@ const bruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bruToJson
|
||||
const bruToEnvJson = (bru) => {
|
||||
try {
|
||||
return bruToEnvJsonV2(bru);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
_.each(variables, (variable) => {
|
||||
if(variable.enabled) {
|
||||
envVars[variable.name] = Mustache.escape(variable.value);
|
||||
}
|
||||
});
|
||||
|
||||
return envVars;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToEnvJson,
|
||||
getEnvVars
|
||||
};
|
||||
20
packages/bruno-cli/src/utils/common.js
Normal file
20
packages/bruno-cli/src/utils/common.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const lpad = (str, width) => {
|
||||
let paddedStr = str;
|
||||
while (paddedStr.length < width) {
|
||||
paddedStr = ' ' + paddedStr;
|
||||
}
|
||||
return paddedStr;
|
||||
};
|
||||
|
||||
const rpad = (str, width) => {
|
||||
let paddedStr = str;
|
||||
while (paddedStr.length < width) {
|
||||
paddedStr = paddedStr + ' ';
|
||||
}
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lpad,
|
||||
rpad
|
||||
};
|
||||
@@ -103,6 +103,21 @@ const stripExtension = (filename = '') => {
|
||||
return filename.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
|
||||
const getSubDirectories = (dir) => {
|
||||
try {
|
||||
const files = fs.readdirSync(dir);
|
||||
const subDirectories = files
|
||||
.filter((file) => {
|
||||
return fs.lstatSync(path.join(dir, file)).isDirectory();
|
||||
})
|
||||
.sort();
|
||||
|
||||
return subDirectories;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
exists,
|
||||
isSymbolicLink,
|
||||
@@ -115,5 +130,6 @@ module.exports = {
|
||||
createDirectory,
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
stripExtension
|
||||
stripExtension,
|
||||
getSubDirectories
|
||||
};
|
||||
|
||||
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.0",
|
||||
"version": "0.10.2",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -9,21 +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/lang": "0.1.0",
|
||||
"@usebruno/schema": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"atob": "^2.1.2",
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"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",
|
||||
@@ -31,7 +29,6 @@
|
||||
"graphql": "^16.6.0",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"qs": "^6.11.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
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ const template = [
|
||||
submenu: [
|
||||
{ role: 'undo'},
|
||||
{ role: 'redo'},
|
||||
{ role: 'separator'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'cut'},
|
||||
{ role: 'copy'},
|
||||
{ role: 'paste'}
|
||||
@@ -27,13 +27,12 @@ const template = [
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload'},
|
||||
{ role: 'toggledevtools'},
|
||||
{ role: 'separator'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'resetzoom'},
|
||||
{ role: 'zoomin'},
|
||||
{ role: 'zoomout'},
|
||||
{ role: 'separator'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'togglefullscreen'}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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', ''),
|
||||
};
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// Recursive function to parse the collection items and create files/folders
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach(item => {
|
||||
if (item.type === 'http-request') {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = jsonToBru(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
|
||||
@@ -3,16 +3,13 @@ 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');
|
||||
const { uuid } = require('../../utils/common');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const {
|
||||
sortFolder,
|
||||
getAllRequestsInFolderRecursively
|
||||
} = require('./helper');
|
||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@@ -99,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
|
||||
});
|
||||
@@ -130,18 +141,44 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
const response = await axios(request);
|
||||
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, response, envVars, collectionVariables, collectionPath);
|
||||
// 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', {
|
||||
environment: result.environment,
|
||||
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', {
|
||||
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();
|
||||
@@ -229,6 +266,13 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
folder = collection;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-started',
|
||||
isRecursive: recursive,
|
||||
collectionUid,
|
||||
folderUid
|
||||
});
|
||||
|
||||
try {
|
||||
const envVars = getEnvVars(environment);
|
||||
let folderRequests = [];
|
||||
@@ -280,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:
|
||||
@@ -308,22 +361,52 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
// send request
|
||||
timeStart = Date.now();
|
||||
const response = await axios(request);
|
||||
timeEnd = Date.now();
|
||||
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, response, envVars, collectionVariables, collectionPath);
|
||||
// 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', {
|
||||
environment: result.environment,
|
||||
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', {
|
||||
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();
|
||||
@@ -374,6 +457,12 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
collectionUid,
|
||||
folderUid
|
||||
});
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'error',
|
||||
|
||||
@@ -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.0",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -9,7 +9,19 @@
|
||||
"peerDependencies": {
|
||||
"vm2": "^3.9.13"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest --testPathIgnorePatterns test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"json-query": "^2.2.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",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
8
packages/bruno-js/readme.md
Normal file
8
packages/bruno-js/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# bruno-js
|
||||
|
||||
Provides the script, test, vars and assert runtimes.
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
class Bru {
|
||||
constructor(environment, collectionVariables) {
|
||||
this._environment = environment;
|
||||
constructor(envVariables, collectionVariables) {
|
||||
this._envVariables = envVariables;
|
||||
this._collectionVariables = collectionVariables;
|
||||
}
|
||||
|
||||
getEnvVar(key) {
|
||||
return this._environment[key];
|
||||
return this._envVariables[key];
|
||||
}
|
||||
|
||||
setEnvVar(key, value) {
|
||||
@@ -15,11 +15,11 @@ class Bru {
|
||||
}
|
||||
|
||||
// gracefully ignore if key is not present in environment
|
||||
if(!this._environment.hasOwnProperty(key)) {
|
||||
if(!this._envVariables.hasOwnProperty(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._environment[key] = value;
|
||||
this._envVariables[key] = value;
|
||||
}
|
||||
|
||||
setVar(key, value) {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
const {
|
||||
ScriptRuntime
|
||||
} = require('./script-runtime');
|
||||
|
||||
const {
|
||||
TestRuntime
|
||||
} = require('./test-runtime');
|
||||
|
||||
const {
|
||||
VarsRuntime
|
||||
} = require('./vars-runtime');
|
||||
const ScriptRuntime = require('./runtime/script-runtime');
|
||||
const TestRuntime = require('./runtime/test-runtime');
|
||||
const VarsRuntime = require('./runtime/vars-runtime');
|
||||
const AssertRuntime = require('./runtime/assert-runtime');
|
||||
|
||||
module.exports = {
|
||||
ScriptRuntime,
|
||||
TestRuntime,
|
||||
VarsRuntime
|
||||
VarsRuntime,
|
||||
AssertRuntime
|
||||
};
|
||||
|
||||
302
packages/bruno-js/src/runtime/assert-runtime.js
Normal file
302
packages/bruno-js/src/runtime/assert-runtime.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const _ = require('lodash');
|
||||
const chai = require('chai');
|
||||
const { nanoid } = require('nanoid');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
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
|
||||
* 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 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);
|
||||
if(!enabledAssertions.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
const bruContext = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
|
||||
const context = {
|
||||
...envVariables,
|
||||
...collectionVariables,
|
||||
...bruContext
|
||||
}
|
||||
|
||||
const assertionResults = [];
|
||||
|
||||
// parse assertion operators
|
||||
for (const v of enabledAssertions) {
|
||||
const lhsExpr = v.name;
|
||||
const rhsExpr = v.value;
|
||||
const {
|
||||
operator,
|
||||
value: rhsOperand
|
||||
} = parseAssertionOperator(rhsExpr);
|
||||
|
||||
try {
|
||||
const lhs = evaluateJsExpression(lhsExpr, context);
|
||||
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
|
||||
|
||||
switch(operator) {
|
||||
case 'eq':
|
||||
expect(lhs).to.equal(rhs);
|
||||
break;
|
||||
case 'neq':
|
||||
expect(lhs).to.not.equal(rhs);
|
||||
break;
|
||||
case 'gt':
|
||||
expect(lhs).to.be.greaterThan(rhs);
|
||||
break;
|
||||
case 'gte':
|
||||
expect(lhs).to.be.greaterThanOrEqual(rhs);
|
||||
break;
|
||||
case 'lt':
|
||||
expect(lhs).to.be.lessThan(rhs);
|
||||
break;
|
||||
case 'lte':
|
||||
expect(lhs).to.be.lessThanOrEqual(rhs);
|
||||
break;
|
||||
case 'in':
|
||||
expect(lhs).to.be.oneOf(rhs);
|
||||
break;
|
||||
case 'notIn':
|
||||
expect(lhs).to.not.be.oneOf(rhs);
|
||||
break;
|
||||
case 'contains':
|
||||
expect(lhs).to.include(rhs);
|
||||
break;
|
||||
case 'notContains':
|
||||
expect(lhs).to.not.include(rhs);
|
||||
break;
|
||||
case 'length':
|
||||
expect(lhs).to.have.lengthOf(rhs);
|
||||
break;
|
||||
case 'matches':
|
||||
expect(lhs).to.match(new RegExp(rhs));
|
||||
break;
|
||||
case 'notMatches':
|
||||
expect(lhs).to.not.match(new RegExp(rhs));
|
||||
break;
|
||||
case 'startsWith':
|
||||
expect(lhs).to.startWith(rhs);
|
||||
break;
|
||||
case 'endsWith':
|
||||
expect(lhs).to.endWith(rhs);
|
||||
break;
|
||||
case 'between':
|
||||
const [min, max] = value.split(',');
|
||||
expect(lhs).to.be.within(min, max);
|
||||
break;
|
||||
case 'isEmpty':
|
||||
expect(lhs).to.be.empty;
|
||||
break;
|
||||
case 'isNull':
|
||||
expect(lhs).to.be.null;
|
||||
break;
|
||||
case 'isUndefined':
|
||||
expect(lhs).to.be.undefined;
|
||||
break;
|
||||
case 'isDefined':
|
||||
expect(lhs).to.not.be.undefined;
|
||||
break;
|
||||
case 'isTruthy':
|
||||
expect(lhs).to.be.true;
|
||||
break;
|
||||
case 'isFalsy':
|
||||
expect(lhs).to.be.false;
|
||||
break;
|
||||
case 'isJson':
|
||||
expect(lhs).to.be.json;
|
||||
break;
|
||||
case 'isNumber':
|
||||
expect(lhs).to.be.a('number');
|
||||
break;
|
||||
case 'isString':
|
||||
expect(lhs).to.be.a('string');
|
||||
break;
|
||||
case 'isBoolean':
|
||||
expect(lhs).to.be.a('boolean');
|
||||
break;
|
||||
default:
|
||||
expect(lhs).to.equal(rhs);
|
||||
break;
|
||||
}
|
||||
|
||||
assertionResults.push({
|
||||
uid: nanoid(),
|
||||
lhsExpr,
|
||||
rhsExpr,
|
||||
rhsOperand,
|
||||
operator,
|
||||
status: 'pass'
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
assertionResults.push({
|
||||
uid: nanoid(),
|
||||
lhsExpr,
|
||||
rhsExpr,
|
||||
rhsOperand,
|
||||
operator,
|
||||
status: 'fail',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return assertionResults;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AssertRuntime;
|
||||
@@ -1,8 +1,15 @@
|
||||
const { NodeVM } = require('vm2');
|
||||
const path = require('path');
|
||||
const Bru = require('./bru');
|
||||
const BrunoRequest = require('./bruno-request');
|
||||
const BrunoResponse = require('./bruno-response');
|
||||
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');
|
||||
|
||||
// Inbuilt Library Support
|
||||
const atob = require('atob');
|
||||
@@ -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,32 +40,52 @@ 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, 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);
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
const vm = new NodeVM({
|
||||
@@ -82,12 +110,10 @@ class ScriptRuntime {
|
||||
|
||||
return {
|
||||
response,
|
||||
environment,
|
||||
envVariables,
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ScriptRuntime
|
||||
};
|
||||
module.exports = ScriptRuntime;
|
||||
@@ -1,11 +1,11 @@
|
||||
const { NodeVM } = require('vm2');
|
||||
const chai = require('chai');
|
||||
const path = require('path');
|
||||
const Bru = require('./bru');
|
||||
const BrunoRequest = require('./bruno-request');
|
||||
const BrunoResponse = require('./bruno-response');
|
||||
const Test = require('./test');
|
||||
const TestResults = require('./test-results');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const BrunoResponse = require('../bruno-response');
|
||||
const Test = require('../test');
|
||||
const TestResults = require('../test-results');
|
||||
|
||||
// Inbuilt Library Support
|
||||
const atob = require('atob');
|
||||
@@ -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,13 +60,11 @@ class TestRuntime {
|
||||
|
||||
return {
|
||||
request,
|
||||
environment,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
results: __brunoTestResults.getResults()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TestRuntime
|
||||
};
|
||||
module.exports = TestRuntime;
|
||||
71
packages/bruno-js/src/runtime/vars-runtime.js
Normal file
71
packages/bruno-js/src/runtime/vars-runtime.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const _ = require('lodash');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
|
||||
class VarsRuntime {
|
||||
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) {
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if(!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const bruContext = {
|
||||
bru,
|
||||
req
|
||||
};
|
||||
|
||||
const context = {
|
||||
...envVariables,
|
||||
...collectionVariables,
|
||||
...bruContext
|
||||
}
|
||||
|
||||
_.each(enabledVars, (v) => {
|
||||
const value = evaluateJsTemplateLiteral(v.value, context);
|
||||
bru.setVar(v.name, value);
|
||||
});
|
||||
|
||||
return {
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
|
||||
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if(!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
const bruContext = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
|
||||
const context = {
|
||||
...envVariables,
|
||||
...collectionVariables,
|
||||
...bruContext
|
||||
}
|
||||
|
||||
_.each(enabledVars, (v) => {
|
||||
const value = evaluateJsExpression(v.value, context);
|
||||
bru.setVar(v.name, value);
|
||||
});
|
||||
|
||||
return {
|
||||
envVariables,
|
||||
collectionVariables
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VarsRuntime;
|
||||
@@ -1,4 +1,4 @@
|
||||
const {nanoid} = require('nanoid');
|
||||
const { nanoid } = require('nanoid');
|
||||
|
||||
class TestResults {
|
||||
constructor() {
|
||||
|
||||
126
packages/bruno-js/src/utils.js
Normal file
126
packages/bruno-js/src/utils.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const jsonQuery = require('json-query');
|
||||
const { get } = require("@usebruno/query");
|
||||
|
||||
const JS_KEYWORDS = `
|
||||
break case catch class const continue debugger default delete do
|
||||
else export extends false finally for function if import in instanceof
|
||||
new null return super switch this throw true try typeof var void while with
|
||||
undefined let static yield arguments of
|
||||
`.split(/\s+/).filter(word => word.length > 0);
|
||||
|
||||
/**
|
||||
* Creates a function from a Javascript expression
|
||||
*
|
||||
* When the function is called, the variables used in this expression are picked up from the context
|
||||
*
|
||||
* ```js
|
||||
* res.data.pets.map(pet => pet.name.toUpperCase())
|
||||
*
|
||||
* function(context) {
|
||||
* const { res, pet } = context;
|
||||
* return res.data.pets.map(pet => pet.name.toUpperCase())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const compileJsExpression = (expr) => {
|
||||
// get all dotted identifiers (foo, bar.baz, .baz)
|
||||
const matches = expr.match(/([\w\.$]+)/g) ?? [];
|
||||
|
||||
// get valid js identifiers (foo, bar)
|
||||
const vars = new Set(
|
||||
matches
|
||||
.filter(match => /^[a-zA-Z$_]/.test(match)) // starts with valid js identifier (foo.bar)
|
||||
.map(match => match.split('.')[0]) // top level identifier (foo)
|
||||
.filter(name => !JS_KEYWORDS.includes(name)) // exclude js keywords
|
||||
);
|
||||
|
||||
// globals such as Math
|
||||
const globals = [...vars].filter(name => name in globalThis);
|
||||
|
||||
const code = {
|
||||
vars: [...vars].join(", "),
|
||||
// pick global from context or globalThis
|
||||
globals: globals
|
||||
.map(name => ` ${name} = ${name} ?? globalThis.${name};`)
|
||||
.join('')
|
||||
};
|
||||
|
||||
const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`;
|
||||
|
||||
return new Function("context", body);
|
||||
};
|
||||
|
||||
const internalExpressionCache = new Map();
|
||||
|
||||
const evaluateJsExpression = (expression, context) => {
|
||||
let fn = internalExpressionCache.get(expression);
|
||||
if (fn == null) {
|
||||
internalExpressionCache.set(expression, fn = compileJsExpression(expression));
|
||||
}
|
||||
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, ...fns) => {
|
||||
return get(response.data, expr, ...fns);
|
||||
};
|
||||
|
||||
res.status = response.status;
|
||||
res.statusText = response.statusText;
|
||||
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,46 +0,0 @@
|
||||
const Bru = require('./bru');
|
||||
const BrunoRequest = require('./bruno-request');
|
||||
const BrunoResponse = require('./bruno-response');
|
||||
const _ = require('lodash');
|
||||
const jsonQuery = require('json-query');
|
||||
|
||||
|
||||
class VarsRuntime {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
runResponseVars(vars, request, response, environment, collectionVariables, collectionPath) {
|
||||
const bru = new Bru(environment, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
res.q = function(expr) {
|
||||
const output = jsonQuery(expr, {data: res.body});
|
||||
|
||||
return output.value;
|
||||
}
|
||||
|
||||
const result = {
|
||||
|
||||
};
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
}
|
||||
console.log(JSON.stringify(vars, null, 2));
|
||||
|
||||
_.each(vars, (v) => {
|
||||
result[v.name] = eval(v.value, context);
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VarsRuntime
|
||||
};
|
||||
140
packages/bruno-js/tests/utils.spec.js
Normal file
140
packages/bruno-js/tests/utils.spec.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const { describe, it, expect } = require("@jest/globals");
|
||||
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require("../src/utils");
|
||||
|
||||
describe("utils", () => {
|
||||
describe("expression evaluation", () => {
|
||||
const context = {
|
||||
res: {
|
||||
data: { pets: ["bruno", "max"] }
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => cache.clear());
|
||||
afterEach(() => cache.clear());
|
||||
|
||||
it("should evaluate expression", () => {
|
||||
let result;
|
||||
|
||||
result = evaluateJsExpression("res.data.pets", context);
|
||||
expect(result).toEqual(["bruno", "max"]);
|
||||
|
||||
result = evaluateJsExpression("res.data.pets[0].toUpperCase()", context);
|
||||
expect(result).toEqual("BRUNO");
|
||||
});
|
||||
|
||||
it("should cache expression", () => {
|
||||
expect(cache.size).toBe(0);
|
||||
evaluateJsExpression("res.data.pets", context);
|
||||
expect(cache.size).toBe(1);
|
||||
});
|
||||
|
||||
it("should use cached expression", () => {
|
||||
const expr = "res.data.pets";
|
||||
|
||||
evaluateJsExpression(expr, context);
|
||||
|
||||
const fn = cache.get(expr);
|
||||
expect(fn).toBeDefined();
|
||||
|
||||
evaluateJsExpression(expr, context);
|
||||
|
||||
// cache should not be overwritten
|
||||
expect(cache.get(expr)).toBe(fn);
|
||||
});
|
||||
|
||||
it("should identify top level variables", () => {
|
||||
const expr = "res.data.pets[0].toUpperCase()";
|
||||
evaluateJsExpression(expr, context);
|
||||
expect(cache.get(expr).toString()).toContain("let { res } = context;");
|
||||
});
|
||||
|
||||
it("should not duplicate variables", () => {
|
||||
const expr = "res.data.pets[0] + res.data.pets[1]";
|
||||
evaluateJsExpression(expr, context);
|
||||
expect(cache.get(expr).toString()).toContain("let { res } = context;");
|
||||
});
|
||||
|
||||
it("should exclude js keywords like true false from vars", () => {
|
||||
const expr = "res.data.pets.length > 0 ? true : false";
|
||||
evaluateJsExpression(expr, context);
|
||||
expect(cache.get(expr).toString()).toContain("let { res } = context;");
|
||||
});
|
||||
|
||||
it("should exclude numbers from vars", () => {
|
||||
const expr = "res.data.pets.length + 10";
|
||||
evaluateJsExpression(expr, context);
|
||||
expect(cache.get(expr).toString()).toContain("let { res } = context;");
|
||||
});
|
||||
|
||||
it("should pick variables from complex expressions", () => {
|
||||
const expr = "res.data.pets.map(pet => pet.length)";
|
||||
const result = evaluateJsExpression(expr, context);
|
||||
expect(result).toEqual([5, 3]);
|
||||
expect(cache.get(expr).toString()).toContain("let { res, pet } = context;");
|
||||
});
|
||||
|
||||
it("should be ok picking extra vars from strings", () => {
|
||||
const expr = "'hello' + ' ' + res.data.pets[0]";
|
||||
const result = evaluateJsExpression(expr, context);
|
||||
expect(result).toBe("hello bruno");
|
||||
// extra var hello is harmless
|
||||
expect(cache.get(expr).toString()).toContain("let { hello, res } = context;");
|
||||
});
|
||||
|
||||
it("should evaluate expressions referencing globals", () => {
|
||||
const startTime = new Date("2022-02-01").getTime();
|
||||
const currentTime = new Date("2022-02-02").getTime();
|
||||
|
||||
jest.useFakeTimers({ now: currentTime });
|
||||
|
||||
const expr = "Math.max(Date.now(), startTime)";
|
||||
const result = evaluateJsExpression(expr, { startTime });
|
||||
|
||||
expect(result).toBe(currentTime);
|
||||
|
||||
expect(cache.get(expr).toString()).toContain("Math = Math ?? globalThis.Math;");
|
||||
expect(cache.get(expr).toString()).toContain("Date = Date ?? globalThis.Date;");
|
||||
});
|
||||
|
||||
it("should use global overridden in context", () => {
|
||||
const startTime = new Date("2022-02-01").getTime();
|
||||
const currentTime = new Date("2022-02-02").getTime();
|
||||
|
||||
jest.useFakeTimers({ now: currentTime });
|
||||
|
||||
const context = {
|
||||
Date: { now: () => new Date("2022-01-31").getTime() },
|
||||
startTime
|
||||
};
|
||||
|
||||
const expr = "Math.max(Date.now(), startTime)";
|
||||
const result = evaluateJsExpression(expr, context);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/lang",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.2",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"arcsecond": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ohm-js": "^16.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# bruno-lang
|
||||
|
||||
The language utils for working with `.bru` files
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
@@ -33,15 +33,22 @@ const grammar = ohm.grammar(`Bru {
|
||||
stnl = st | nl
|
||||
tagend = nl "}"
|
||||
optionalnl = ~tagend nl
|
||||
validkey = ~(st | ":") any
|
||||
validvalue = ~nl any
|
||||
keychar = ~(tagend | st | nl | ":") any
|
||||
valuechar = ~(nl | tagend) any
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* key st* ":" st* value? st*
|
||||
key = ~tagend validkey*
|
||||
value = ~tagend validvalue*
|
||||
pair = st* key st* ":" st* value st*
|
||||
key = keychar*
|
||||
value = valuechar*
|
||||
|
||||
// Dictionary for Assert Block
|
||||
assertdictionary = st* "{" assertpairlist? tagend
|
||||
assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*
|
||||
assertpair = st* assertkey st* ":" st* value st*
|
||||
assertkey = ~tagend assertkeychar*
|
||||
assertkeychar = ~(tagend | nl | ":") any
|
||||
|
||||
// Text Blocks
|
||||
textblock = textline (~tagend nl textline)*
|
||||
@@ -50,11 +57,12 @@ const grammar = ohm.grammar(`Bru {
|
||||
|
||||
meta = "meta" dictionary
|
||||
|
||||
http = get | post | put | delete | options | head | connect | trace
|
||||
http = get | post | put | delete | patch | options | head | connect | trace
|
||||
get = "get" dictionary
|
||||
post = "post" dictionary
|
||||
put = "put" dictionary
|
||||
delete = "delete" dictionary
|
||||
patch = "patch" dictionary
|
||||
options = "options" dictionary
|
||||
head = "head" dictionary
|
||||
connect = "connect" dictionary
|
||||
@@ -67,7 +75,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
varsandassert = varsreq | varsres | assert
|
||||
varsreq = "vars:pre-request" dictionary
|
||||
varsres = "vars:post-response" dictionary
|
||||
assert = "assert" dictionary
|
||||
assert = "assert" assertdictionary
|
||||
|
||||
body = "body" st* "{" nl* textblock tagend
|
||||
bodyjson = "body:json" st* "{" nl* textblock tagend
|
||||
@@ -139,7 +147,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
let res = {};
|
||||
res[key.ast] = _.get(value, 'ast[0]', '');
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
key(chars) {
|
||||
@@ -148,6 +156,20 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
value(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
assertdictionary(_1, _2, pairlist, _3) {
|
||||
return pairlist.ast;
|
||||
},
|
||||
assertpairlist(_1, pair, _2, rest, _3) {
|
||||
return [pair.ast, ...rest.ast];
|
||||
},
|
||||
assertpair(_1, key, _2, _3, _4, value, _5) {
|
||||
let res = {};
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
assertkey(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
textblock(line, _1, rest) {
|
||||
return [line.ast, ...rest.ast].join('\n');
|
||||
},
|
||||
@@ -216,6 +238,14 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
patch(_1, dictionary) {
|
||||
return {
|
||||
http: {
|
||||
method: 'patch',
|
||||
...mapPairListToKeyValPair(dictionary.ast)
|
||||
}
|
||||
};
|
||||
},
|
||||
options(_1, dictionary) {
|
||||
return {
|
||||
http: {
|
||||
@@ -351,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) {
|
||||
|
||||
@@ -6,16 +6,18 @@ const grammar = ohm.grammar(`Bru {
|
||||
|
||||
nl = "\\r"? "\\n"
|
||||
st = " " | "\\t"
|
||||
stnl = st | nl
|
||||
tagend = nl "}"
|
||||
validkey = ~(st | ":") any
|
||||
validvalue = ~nl any
|
||||
optionalnl = ~tagend nl
|
||||
keychar = ~(tagend | st | nl | ":") any
|
||||
valuechar = ~(nl | tagend) any
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = nl* pair (~tagend nl pair)* (~tagend space)*
|
||||
pair = st* key st* ":" st* value? st*
|
||||
key = ~tagend validkey*
|
||||
value = ~tagend validvalue*
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* key st* ":" st* value st*
|
||||
key = keychar*
|
||||
value = valuechar*
|
||||
|
||||
vars = "vars" dictionary
|
||||
}`);
|
||||
@@ -68,7 +70,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
let res = {};
|
||||
res[key.ast] = _.get(value, 'ast[0]', '');
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
key(chars) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
24
packages/bruno-lang/v2/tests/assert.spec.js
Normal file
24
packages/bruno-lang/v2/tests/assert.spec.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This test file is used to test the text parser.
|
||||
*/
|
||||
const parser = require("../src/bruToJson");
|
||||
|
||||
describe("assert parser", () => {
|
||||
it("should parse assert statement", () => {
|
||||
const input = `
|
||||
assert {
|
||||
res("data.airports").filter(a => a.code ==="BLR").name: "Bangalore International Airport"
|
||||
}
|
||||
`;
|
||||
|
||||
const output = parser(input);
|
||||
const expected = {
|
||||
"assertions": [{
|
||||
name: "res(\"data.airports\").filter(a => a.code ===\"BLR\").name",
|
||||
value: '"Bangalore International Airport"',
|
||||
enabled: true
|
||||
}]
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* This test file is used to test the text parser.
|
||||
* This test file is used to test the dictionary parser.
|
||||
*/
|
||||
|
||||
const parser = require("../src/bruToJson");
|
||||
@@ -117,6 +117,24 @@ headers {
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should parse empty url", () => {
|
||||
const input = `
|
||||
get {
|
||||
url:
|
||||
body: json
|
||||
}`;
|
||||
|
||||
const output = parser(input);
|
||||
const expected = {
|
||||
"http": {
|
||||
"url": "",
|
||||
"method": "get",
|
||||
"body": "json",
|
||||
}
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should throw error on invalid header", () => {
|
||||
const input = `
|
||||
headers {
|
||||
|
||||
@@ -85,4 +85,33 @@ vars {
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should parse vars with empty values", () => {
|
||||
const input = `
|
||||
vars {
|
||||
url:
|
||||
phone:
|
||||
api-key:
|
||||
}
|
||||
`;
|
||||
|
||||
const output = parser(input);
|
||||
const expected = {
|
||||
"variables": [{
|
||||
"name": "url",
|
||||
"value": "",
|
||||
"enabled" : true,
|
||||
}, {
|
||||
"name": "phone",
|
||||
"value": "",
|
||||
"enabled" : true,
|
||||
}, {
|
||||
"name": "api-key",
|
||||
"value": "",
|
||||
"enabled" : true,
|
||||
}]
|
||||
};
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user