mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 15:33:11 +01:00
feature(postman_tests_scripts): automatic tests and scripts translation from postman import (#1151)
* feature(postman_tests_scripts): automatic tests and scripts translation from postman import --------- Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local> Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
This commit is contained in:
parent
2cd0e065bd
commit
410eecc884
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
bun.lockb
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
@ -17,6 +17,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
@ -7,6 +7,13 @@ import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [options, setOptions] = useState({
|
||||
enablePostmanTranslations: {
|
||||
enabled: true,
|
||||
label: 'Auto translate postman scripts',
|
||||
subLabel: "When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
|
||||
}
|
||||
})
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then((collection) => {
|
||||
@ -16,7 +23,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
};
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
importPostmanCollection(options)
|
||||
.then((collection) => {
|
||||
handleSubmit(collection);
|
||||
})
|
||||
@ -38,21 +45,66 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
};
|
||||
|
||||
const toggleOptions = (event, optionKey) => {
|
||||
setOptions({ ...options, [optionKey]: {
|
||||
...options[optionKey],
|
||||
enabled: !options[optionKey].enabled
|
||||
} });
|
||||
};
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportBrunoCollection}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
<CollectionButton onClick={handleImportBrunoCollection}>
|
||||
Bruno Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportPostmanCollection}>
|
||||
</CollectionButton>
|
||||
<CollectionButton onClick={handleImportPostmanCollection}>
|
||||
Postman Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
|
||||
</CollectionButton>
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>
|
||||
Insomnia Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
|
||||
</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>
|
||||
OpenAPI V3 Spec
|
||||
</CollectionButton>
|
||||
</div>
|
||||
<div className="flex justify-start w-full mt-4 max-w-[450px]">
|
||||
{Object.entries(options || {}).map(([key, option]) => (
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
aria-describedby="comments-description"
|
||||
name="comments"
|
||||
type="checkbox"
|
||||
checked={option.enabled}
|
||||
onChange={(e) => toggleOptions(e,key)}
|
||||
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 text-sm leading-6">
|
||||
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
|
||||
{option.label}
|
||||
</label>
|
||||
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
|
||||
{option.subLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -45,7 +45,11 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Import Collection"
|
||||
confirmText="Import"
|
||||
handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
|
@ -26,7 +26,7 @@ const TitleBar = () => {
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
const handleImportCollectionLocation = (collectionLocation, useTranslation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
|
@ -29,8 +29,8 @@ const Welcome = () => {
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
const handleImportCollectionLocation = (collectionLocation, enableTRanslation = true) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation, enableTranslation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
|
@ -9,20 +9,31 @@ export const ThemeProvider = (props) => {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
||||
const toggleHtml = () => {
|
||||
const html = document.querySelector('html');
|
||||
if (html) {
|
||||
html.classList.toggle('dark');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||||
if (storedTheme !== 'system') return;
|
||||
setDisplayedTheme(e.matches ? 'light' : 'dark');
|
||||
toggleHtml();
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove("light", "dark");
|
||||
if (storedTheme === 'system') {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
setDisplayedTheme(isBrowserThemeLight ? 'light' : 'dark');
|
||||
root.classList.add(isBrowserThemeLight ? 'light' : 'dark')
|
||||
} else {
|
||||
setDisplayedTheme(storedTheme);
|
||||
root.classList.add(storedTheme)
|
||||
}
|
||||
}, [storedTheme, setDisplayedTheme, window.matchMedia]);
|
||||
|
||||
|
@ -4,6 +4,7 @@ import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -53,7 +54,7 @@ const convertV21Auth = (array) => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
|
||||
@ -77,7 +78,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
|
||||
}
|
||||
} else {
|
||||
if (i.request) {
|
||||
@ -121,9 +122,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
brunoRequestItem.request.script.req = event.script.exec.map((line) => `// ${line}`).join('\n');
|
||||
brunoRequestItem.request.script.req = event.script.exec
|
||||
.map((line) => options.enablePostmanTranslations.enabled ?
|
||||
postmanTranslation(line) : `// ${line}`).join('\n')
|
||||
} else {
|
||||
brunoRequestItem.request.script.req = `// ${event.script.exec[0]} `;
|
||||
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled ?
|
||||
postmanTranslation(event.script.exec[0]) :
|
||||
`// ${event.script.exec[0]} `;
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
@ -131,9 +136,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoRequestItem.request.tests = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
brunoRequestItem.request.tests = event.script.exec.map((line) => `// ${line}`).join('\n');
|
||||
brunoRequestItem.request.tests = event.script.exec
|
||||
.map((line) => options.enablePostmanTranslations.enabled ?
|
||||
postmanTranslation(line) : `// ${line}`).join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.tests = `// ${event.script.exec[0]} `;
|
||||
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled ?
|
||||
postmanTranslation(event.script.exec[0]) :
|
||||
`// ${event.script.exec[0]} `;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -263,7 +272,7 @@ const searchLanguageByHeader = (headers) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const importPostmanV2Collection = (collection, options) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name,
|
||||
uid: uuid(),
|
||||
@ -272,12 +281,12 @@ const importPostmanV2Collection = (collection) => {
|
||||
environments: []
|
||||
};
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
const parsePostmanCollection = (str) => {
|
||||
const parsePostmanCollection = (str, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let collection = JSON.parse(str);
|
||||
@ -289,7 +298,7 @@ const parsePostmanCollection = (str) => {
|
||||
];
|
||||
|
||||
if (v2Schemas.includes(schema)) {
|
||||
return resolve(importPostmanV2Collection(collection));
|
||||
return resolve(importPostmanV2Collection(collection, options));
|
||||
}
|
||||
|
||||
throw new BrunoError('Unknown postman schema');
|
||||
@ -304,11 +313,11 @@ const parsePostmanCollection = (str) => {
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
const importCollection = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanCollection)
|
||||
.then((str) => parsePostmanCollection(str, options))
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
|
@ -0,0 +1,27 @@
|
||||
const replacements = {
|
||||
'pm\\.environment\\.get\\(([\'"])([^\'"]*)\\1\\)': 'bru.getEnvVar($1$2$1)',
|
||||
'pm\\.environment\\.set\\(([\'"])([^\'"]*)\\1, ([\'"])([^\'"]*)\\3\\)': 'bru.setEnvVar($1$2$1, $3$4$3)',
|
||||
'pm\\.variables\\.get\\(([\'"])([^\'"]*)\\1\\)': 'bru.getVar($1$2$1)',
|
||||
'pm\\.variables\\.set\\(([\'"])([^\'"]*)\\1, ([\'"])([^\'"]*)\\3\\)': 'bru.setVar($1$2$1, $3$4$3)'
|
||||
};
|
||||
|
||||
export const postmanTranslation = (script) => {
|
||||
try {
|
||||
const modifiedScript = Object.entries(replacements || {})
|
||||
.map(([pattern, replacement]) => {
|
||||
const regex = new RegExp(pattern, 'g');
|
||||
return script?.replace(regex, replacement);
|
||||
})
|
||||
.find((modified) => modified !== script);
|
||||
if (modifiedScript) {
|
||||
// translation successful
|
||||
return modifiedScript;
|
||||
} else {
|
||||
// non-translatable script
|
||||
return script?.includes('pm.') ? `// ${script}` : script;
|
||||
}
|
||||
} catch (e) {
|
||||
// non-translatable script
|
||||
return script?.includes('pm.') ? `// ${script}` : script;
|
||||
}
|
||||
};
|
@ -1,8 +1,22 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{js,jsx}',
|
||||
'./components/**/*.{js,jsx}',
|
||||
'./app/**/*.{js,jsx}',
|
||||
'./src/**/*.{js,jsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
extend: {}
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms")],
|
||||
}
|
@ -403,7 +403,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation, enableTranslation) => {
|
||||
try {
|
||||
let collectionName = sanitizeDirectoryName(collection.name);
|
||||
let collectionPath = path.join(collectionLocation, collectionName);
|
||||
|
Loading…
Reference in New Issue
Block a user