feat: testing support has arrived !

This commit is contained in:
Anoop M D 2023-01-29 17:35:28 +05:30
parent cc261326fc
commit c328281f21
13 changed files with 256 additions and 19 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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">
&#x2714;&nbsp; {result.description}
</span>
) : (
<>
<span className="test-failure">
&#x2718;&nbsp; {result.description}
</span>
<br />
<span className="error-message pl-8">
{result.error}
</span>
</>
)}
</li>
))}
</ul>
</StyledWrapper>
);
};
export default TestResults;

View File

@ -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} />

View File

@ -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]);
};

View File

@ -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;

View File

@ -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",

View File

@ -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

View File

@ -2,6 +2,11 @@ const {
ScriptRuntime
} = require('./scripts/script-runtime');
const {
TestRuntime
} = require('./scripts/test-runtime');
module.exports = {
ScriptRuntime
ScriptRuntime,
TestRuntime
};

View File

@ -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,

View File

@ -0,0 +1,15 @@
class TestResults {
constructor() {
this.results = [];
}
addResult(result) {
this.results.push(result);
}
getResults() {
return this.results;
}
}
module.exports = TestResults;

View 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
};

View 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;