mirror of
https://github.com/usebruno/bruno.git
synced 2025-01-03 12:39:34 +01:00
feat: testing support has arrived !
This commit is contained in:
parent
cc261326fc
commit
c328281f21
@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.some-tests-failed {
|
||||||
|
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-tests-passed {
|
||||||
|
color: ${(props) => props.theme.colors.text.green} !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default StyledWrapper;
|
export default StyledWrapper;
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
.test-success {
|
||||||
|
color: ${(props) => props.theme.colors.text.green};
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-failure {
|
||||||
|
color: ${(props) => props.theme.colors.text.danger};
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: ${(props) => props.theme.colors.text.muted};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const TestResults = ({ results }) => {
|
||||||
|
if (!results || !results.length) {
|
||||||
|
return (
|
||||||
|
<div className="px-3">
|
||||||
|
No tests found
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passedTests = results.filter((result) => result.status === 'pass');
|
||||||
|
const failedTests = results.filter((result) => result.status === 'fail');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className='flex flex-col px-3'>
|
||||||
|
<div className="py-2 font-medium test-summary">
|
||||||
|
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||||
|
</div>
|
||||||
|
<ul className="">
|
||||||
|
{results.map((result, index) => (
|
||||||
|
<li key={index} className="py-1">
|
||||||
|
{result.status === 'pass' ? (
|
||||||
|
<span className="test-success">
|
||||||
|
✔ {result.description}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="test-failure">
|
||||||
|
✘ {result.description}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span className="error-message pl-8">
|
||||||
|
{result.error}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestResults;
|
@ -11,8 +11,33 @@ import StatusCode from './StatusCode';
|
|||||||
import ResponseTime from './ResponseTime';
|
import ResponseTime from './ResponseTime';
|
||||||
import ResponseSize from './ResponseSize';
|
import ResponseSize from './ResponseSize';
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
|
import TestResults from './TestResults';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const TestResultsLabel = ({ results }) => {
|
||||||
|
if(!results || !results.length) {
|
||||||
|
return 'Tests';
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfTests = results.length;
|
||||||
|
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div>Tests</div>
|
||||||
|
{numberOfFailedTests ? (
|
||||||
|
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||||
|
{numberOfFailedTests}
|
||||||
|
</sup>
|
||||||
|
) : (
|
||||||
|
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||||
|
{numberOfTests}
|
||||||
|
</sup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const tabs = useSelector((state) => state.tabs.tabs);
|
const tabs = useSelector((state) => state.tabs.tabs);
|
||||||
@ -46,6 +71,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
case 'timeline': {
|
case 'timeline': {
|
||||||
return <Timeline item={item} />;
|
return <Timeline item={item} />;
|
||||||
}
|
}
|
||||||
|
case 'tests': {
|
||||||
|
return <TestResults results={item.testResults} />;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return <div>404 | Not found</div>;
|
return <div>404 | Not found</div>;
|
||||||
@ -96,6 +124,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||||
Timeline
|
Timeline
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||||
|
<TestResultsLabel results={item.testResults} />
|
||||||
|
</div>
|
||||||
{!isLoading ? (
|
{!isLoading ? (
|
||||||
<div className="flex flex-grow justify-end items-center">
|
<div className="flex flex-grow justify-end items-center">
|
||||||
<StatusCode status={response.status} />
|
<StatusCode status={response.status} />
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
collectionUnlinkEnvFileEvent,
|
collectionUnlinkEnvFileEvent,
|
||||||
requestSentEvent,
|
requestSentEvent,
|
||||||
requestQueuedEvent,
|
requestQueuedEvent,
|
||||||
|
testResultsEvent,
|
||||||
scriptEnvironmentUpdateEvent
|
scriptEnvironmentUpdateEvent
|
||||||
} from 'providers/ReduxStore/slices/collections';
|
} from 'providers/ReduxStore/slices/collections';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@ -95,6 +96,10 @@ const useCollectionTreeSync = () => {
|
|||||||
dispatch(requestQueuedEvent(val));
|
dispatch(requestQueuedEvent(val));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _testResults = (val) => {
|
||||||
|
dispatch(testResultsEvent(val));
|
||||||
|
};
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:ready');
|
ipcRenderer.invoke('renderer:ready');
|
||||||
|
|
||||||
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||||
@ -104,6 +109,7 @@ const useCollectionTreeSync = () => {
|
|||||||
const removeListener5 = ipcRenderer.on('main:http-request-sent', _httpRequestSent);
|
const removeListener5 = ipcRenderer.on('main:http-request-sent', _httpRequestSent);
|
||||||
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
|
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
|
||||||
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
||||||
|
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeListener1();
|
removeListener1();
|
||||||
@ -113,6 +119,7 @@ const useCollectionTreeSync = () => {
|
|||||||
removeListener5();
|
removeListener5();
|
||||||
removeListener6();
|
removeListener6();
|
||||||
removeListener7();
|
removeListener7();
|
||||||
|
removeListener8();
|
||||||
};
|
};
|
||||||
}, [isElectron]);
|
}, [isElectron]);
|
||||||
};
|
};
|
||||||
|
@ -812,6 +812,18 @@ export const collectionsSlice = createSlice({
|
|||||||
collection.environments.push(environment);
|
collection.environments.push(environment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
testResultsEvent: (state, action) => {
|
||||||
|
const { itemUid, collectionUid, results } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.testResults = results;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -861,7 +873,8 @@ export const {
|
|||||||
collectionChangeFileEvent,
|
collectionChangeFileEvent,
|
||||||
collectionUnlinkFileEvent,
|
collectionUnlinkFileEvent,
|
||||||
collectionUnlinkDirectoryEvent,
|
collectionUnlinkDirectoryEvent,
|
||||||
collectionAddEnvFileEvent
|
collectionAddEnvFileEvent,
|
||||||
|
testResultsEvent
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"atob": "^2.1.2",
|
"atob": "^2.1.2",
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"btoa": "^1.2.1",
|
"btoa": "^1.2.1",
|
||||||
|
"chai": "^4.3.7",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
|
@ -2,8 +2,8 @@ const axios = require('axios');
|
|||||||
const Mustache = require('mustache');
|
const Mustache = require('mustache');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const { ipcMain } = require('electron');
|
const { ipcMain } = require('electron');
|
||||||
const { forOwn, extend, each } = require('lodash');
|
const { forOwn, extend, each, get } = require('lodash');
|
||||||
const { ScriptRuntime } = require('@usebruno/js');
|
const { ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||||
const prepareRequest = require('./prepare-request');
|
const prepareRequest = require('./prepare-request');
|
||||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||||
const { uuid } = require('../../utils/common');
|
const { uuid } = require('../../utils/common');
|
||||||
@ -79,12 +79,12 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
const envVars = getEnvVars(environment);
|
const envVars = getEnvVars(environment);
|
||||||
|
|
||||||
if(request.script && request.script.length) {
|
if(request.script && request.script.length) {
|
||||||
let script = request.script + '\n if (typeof onRequest === "function") {onRequest(brunoRequest);}';
|
let script = request.script + '\n if (typeof onRequest === "function") {onRequest(__brunoRequest);}';
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const res = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
|
const result = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
|
||||||
|
|
||||||
mainWindow.webContents.send('main:script-environment-update', {
|
mainWindow.webContents.send('main:script-environment-update', {
|
||||||
environment: res.environment,
|
environment: result.environment,
|
||||||
collectionUid
|
collectionUid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -106,15 +106,27 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
cancelTokenUid
|
cancelTokenUid
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await axios(request);
|
const response = await axios(request);
|
||||||
|
|
||||||
if(request.script && request.script.length) {
|
if(request.script && request.script.length) {
|
||||||
let script = request.script + '\n if (typeof onResponse === "function") {onResponse(brunoResponse);}';
|
let script = request.script + '\n if (typeof onResponse === "function") {onResponse(__brunoResponse);}';
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const res = scriptRuntime.runResponseScript(script, result, envVars, collectionPath);
|
const result = scriptRuntime.runResponseScript(script, response, envVars, collectionPath);
|
||||||
|
|
||||||
mainWindow.webContents.send('main:script-environment-update', {
|
mainWindow.webContents.send('main:script-environment-update', {
|
||||||
environment: res.environment,
|
environment: result.environment,
|
||||||
|
collectionUid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFile = get(item, 'request.tests');
|
||||||
|
if(testFile && testFile.length) {
|
||||||
|
const testRuntime = new TestRuntime();
|
||||||
|
const result = testRuntime.runTests(testFile, request, response, envVars, collectionPath);
|
||||||
|
|
||||||
|
mainWindow.webContents.send('main:test-results', {
|
||||||
|
results: result.results,
|
||||||
|
itemUid: item.uid,
|
||||||
collectionUid
|
collectionUid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -122,10 +134,10 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
deleteCancelToken(cancelTokenUid);
|
deleteCancelToken(cancelTokenUid);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: result.status,
|
status: response.status,
|
||||||
statusText: result.statusText,
|
statusText: response.statusText,
|
||||||
headers: result.headers,
|
headers: response.headers,
|
||||||
data: result.data
|
data: response.data
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// todo: better error handling
|
// todo: better error handling
|
||||||
|
@ -2,6 +2,11 @@ const {
|
|||||||
ScriptRuntime
|
ScriptRuntime
|
||||||
} = require('./scripts/script-runtime');
|
} = require('./scripts/script-runtime');
|
||||||
|
|
||||||
|
const {
|
||||||
|
TestRuntime
|
||||||
|
} = require('./scripts/test-runtime');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ScriptRuntime
|
ScriptRuntime,
|
||||||
|
TestRuntime
|
||||||
};
|
};
|
||||||
|
@ -10,11 +10,11 @@ class ScriptRuntime {
|
|||||||
|
|
||||||
runRequestScript(script, request, environment, collectionPath) {
|
runRequestScript(script, request, environment, collectionPath) {
|
||||||
const bru = new Bru(environment);
|
const bru = new Bru(environment);
|
||||||
const brunoRequest = new BrunoRequest(request);
|
const __brunoRequest = new BrunoRequest(request);
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
bru,
|
bru,
|
||||||
brunoRequest
|
__brunoRequest
|
||||||
};
|
};
|
||||||
const vm = new NodeVM({
|
const vm = new NodeVM({
|
||||||
sandbox: context,
|
sandbox: context,
|
||||||
@ -35,11 +35,11 @@ class ScriptRuntime {
|
|||||||
|
|
||||||
runResponseScript(script, response, environment, collectionPath) {
|
runResponseScript(script, response, environment, collectionPath) {
|
||||||
const bru = new Bru(environment);
|
const bru = new Bru(environment);
|
||||||
const brunoResponse = new BrunoResponse(response);
|
const __brunoResponse = new BrunoResponse(response);
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
bru,
|
bru,
|
||||||
brunoResponse
|
__brunoResponse
|
||||||
};
|
};
|
||||||
const vm = new NodeVM({
|
const vm = new NodeVM({
|
||||||
sandbox: context,
|
sandbox: context,
|
||||||
|
15
packages/bruno-js/src/scripts/test-results.js
Normal file
15
packages/bruno-js/src/scripts/test-results.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
class TestResults {
|
||||||
|
constructor() {
|
||||||
|
this.results = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addResult(result) {
|
||||||
|
this.results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResults() {
|
||||||
|
return this.results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TestResults;
|
55
packages/bruno-js/src/scripts/test-runtime.js
Normal file
55
packages/bruno-js/src/scripts/test-runtime.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
class TestRuntime {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(testsFile, request, response, environment, collectionPath) {
|
||||||
|
const bru = new Bru(environment);
|
||||||
|
const req = new BrunoRequest(request);
|
||||||
|
const res = new BrunoResponse(response);
|
||||||
|
|
||||||
|
const __brunoTestResults = new TestResults();
|
||||||
|
const test = Test(__brunoTestResults, chai);
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
bru,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
test,
|
||||||
|
expect: chai.expect,
|
||||||
|
assert: chai.assert,
|
||||||
|
__brunoTestResults: __brunoTestResults
|
||||||
|
};
|
||||||
|
|
||||||
|
const vm = new NodeVM({
|
||||||
|
sandbox: context,
|
||||||
|
require: {
|
||||||
|
context: 'sandbox',
|
||||||
|
external: true,
|
||||||
|
root: [collectionPath]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(__brunoTestResults);
|
||||||
|
|
||||||
|
vm.run(testsFile, path.join(collectionPath, 'vm.js'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
environment,
|
||||||
|
results: __brunoTestResults.getResults()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TestRuntime
|
||||||
|
};
|
27
packages/bruno-js/src/scripts/test.js
Normal file
27
packages/bruno-js/src/scripts/test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const Test = (__brunoTestResults, chai) => (description, callback) => {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
__brunoTestResults.addResult({ description, status: "pass" });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chai.AssertionError);
|
||||||
|
if (error instanceof chai.AssertionError) {
|
||||||
|
const { message, actual, expected } = error;
|
||||||
|
__brunoTestResults.addResult({
|
||||||
|
description,
|
||||||
|
status: "fail",
|
||||||
|
error: message,
|
||||||
|
actual,
|
||||||
|
expected
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
__brunoTestResults.addResult({
|
||||||
|
description,
|
||||||
|
status: "fail",
|
||||||
|
error: error.message || 'An unexpected error occurred.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Test;
|
Loading…
Reference in New Issue
Block a user