mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-22 06:31:18 +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;
|
||||
|
@ -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 ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
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 dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@ -46,6 +71,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
case 'timeline': {
|
||||
return <Timeline item={item} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
@ -96,6 +124,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} />
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={response.status} />
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
requestSentEvent,
|
||||
requestQueuedEvent,
|
||||
testResultsEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -95,6 +96,10 @@ const useCollectionTreeSync = () => {
|
||||
dispatch(requestQueuedEvent(val));
|
||||
};
|
||||
|
||||
const _testResults = (val) => {
|
||||
dispatch(testResultsEvent(val));
|
||||
};
|
||||
|
||||
ipcRenderer.invoke('renderer:ready');
|
||||
|
||||
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||
@ -104,6 +109,7 @@ const useCollectionTreeSync = () => {
|
||||
const removeListener5 = ipcRenderer.on('main:http-request-sent', _httpRequestSent);
|
||||
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);
|
||||
|
||||
return () => {
|
||||
removeListener1();
|
||||
@ -113,6 +119,7 @@ const useCollectionTreeSync = () => {
|
||||
removeListener5();
|
||||
removeListener6();
|
||||
removeListener7();
|
||||
removeListener8();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
@ -812,6 +812,18 @@ export const collectionsSlice = createSlice({
|
||||
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,
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent
|
||||
collectionAddEnvFileEvent,
|
||||
testResultsEvent
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
@ -20,6 +20,7 @@
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.26.0",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
|
@ -2,8 +2,8 @@ const axios = require('axios');
|
||||
const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend, each } = require('lodash');
|
||||
const { ScriptRuntime } = require('@usebruno/js');
|
||||
const { forOwn, extend, each, get } = require('lodash');
|
||||
const { ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||
const { uuid } = require('../../utils/common');
|
||||
@ -79,12 +79,12 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const envVars = getEnvVars(environment);
|
||||
|
||||
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 res = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
|
||||
const result = scriptRuntime.runRequestScript(script, request, envVars, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
environment: res.environment,
|
||||
environment: result.environment,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
@ -106,15 +106,27 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const result = await axios(request);
|
||||
const response = await axios(request);
|
||||
|
||||
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 res = scriptRuntime.runResponseScript(script, result, envVars, collectionPath);
|
||||
const result = scriptRuntime.runResponseScript(script, response, envVars, collectionPath);
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
@ -122,10 +134,10 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
statusText: result.statusText,
|
||||
headers: result.headers,
|
||||
data: result.data
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
// todo: better error handling
|
||||
|
@ -2,6 +2,11 @@ const {
|
||||
ScriptRuntime
|
||||
} = require('./scripts/script-runtime');
|
||||
|
||||
const {
|
||||
TestRuntime
|
||||
} = require('./scripts/test-runtime');
|
||||
|
||||
module.exports = {
|
||||
ScriptRuntime
|
||||
ScriptRuntime,
|
||||
TestRuntime
|
||||
};
|
||||
|
@ -10,11 +10,11 @@ class ScriptRuntime {
|
||||
|
||||
runRequestScript(script, request, environment, collectionPath) {
|
||||
const bru = new Bru(environment);
|
||||
const brunoRequest = new BrunoRequest(request);
|
||||
const __brunoRequest = new BrunoRequest(request);
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
brunoRequest
|
||||
__brunoRequest
|
||||
};
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
@ -35,11 +35,11 @@ class ScriptRuntime {
|
||||
|
||||
runResponseScript(script, response, environment, collectionPath) {
|
||||
const bru = new Bru(environment);
|
||||
const brunoResponse = new BrunoResponse(response);
|
||||
const __brunoResponse = new BrunoResponse(response);
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
brunoResponse
|
||||
__brunoResponse
|
||||
};
|
||||
const vm = new NodeVM({
|
||||
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