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.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
bun.lockb
|
||||||
node_modules
|
node_modules
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
"@tabler/icons": "^1.46.0",
|
"@tabler/icons": "^1.46.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@usebruno/common": "0.1.0",
|
"@usebruno/common": "0.1.0",
|
||||||
"@usebruno/graphql-docs": "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 importBrunoCollection from 'utils/importers/bruno-collection';
|
||||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||||
@ -7,6 +7,13 @@ import { toastError } from 'utils/common/error';
|
|||||||
import Modal from 'components/Modal';
|
import Modal from 'components/Modal';
|
||||||
|
|
||||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
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 = () => {
|
const handleImportBrunoCollection = () => {
|
||||||
importBrunoCollection()
|
importBrunoCollection()
|
||||||
.then((collection) => {
|
.then((collection) => {
|
||||||
@ -16,7 +23,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleImportPostmanCollection = () => {
|
const handleImportPostmanCollection = () => {
|
||||||
importPostmanCollection()
|
importPostmanCollection(options)
|
||||||
.then((collection) => {
|
.then((collection) => {
|
||||||
handleSubmit(collection);
|
handleSubmit(collection);
|
||||||
})
|
})
|
||||||
@ -38,21 +45,66 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
|||||||
})
|
})
|
||||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
.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 (
|
return (
|
||||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||||
<div>
|
<div className="flex flex-col">
|
||||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportBrunoCollection}>
|
<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
|
Bruno Collection
|
||||||
</div>
|
</CollectionButton>
|
||||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportPostmanCollection}>
|
<CollectionButton onClick={handleImportPostmanCollection}>
|
||||||
Postman Collection
|
Postman Collection
|
||||||
</div>
|
</CollectionButton>
|
||||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
|
<CollectionButton onClick={handleImportInsomniaCollection}>
|
||||||
Insomnia Collection
|
Insomnia Collection
|
||||||
</div>
|
</CollectionButton>
|
||||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
|
<CollectionButton onClick={handleImportOpenapiCollection}>
|
||||||
OpenAPI V3 Spec
|
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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -45,7 +45,11 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
|||||||
const onSubmit = () => formik.handleSubmit();
|
const onSubmit = () => formik.handleSubmit();
|
||||||
|
|
||||||
return (
|
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}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="collectionName" className="block font-semibold">
|
<label htmlFor="collectionName" className="block font-semibold">
|
||||||
|
@ -26,7 +26,7 @@ const TitleBar = () => {
|
|||||||
setImportCollectionLocationModalOpen(true);
|
setImportCollectionLocationModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportCollectionLocation = (collectionLocation) => {
|
const handleImportCollectionLocation = (collectionLocation, useTranslation) => {
|
||||||
dispatch(importCollection(importedCollection, collectionLocation));
|
dispatch(importCollection(importedCollection, collectionLocation));
|
||||||
setImportCollectionLocationModalOpen(false);
|
setImportCollectionLocationModalOpen(false);
|
||||||
setImportedCollection(null);
|
setImportedCollection(null);
|
||||||
|
@ -29,8 +29,8 @@ const Welcome = () => {
|
|||||||
setImportCollectionLocationModalOpen(true);
|
setImportCollectionLocationModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportCollectionLocation = (collectionLocation) => {
|
const handleImportCollectionLocation = (collectionLocation, enableTRanslation = true) => {
|
||||||
dispatch(importCollection(importedCollection, collectionLocation));
|
dispatch(importCollection(importedCollection, collectionLocation, enableTranslation));
|
||||||
setImportCollectionLocationModalOpen(false);
|
setImportCollectionLocationModalOpen(false);
|
||||||
setImportedCollection(null);
|
setImportedCollection(null);
|
||||||
toast.success('Collection imported successfully');
|
toast.success('Collection imported successfully');
|
||||||
|
@ -9,20 +9,31 @@ export const ThemeProvider = (props) => {
|
|||||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
||||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
||||||
|
const toggleHtml = () => {
|
||||||
|
const html = document.querySelector('html');
|
||||||
|
if (html) {
|
||||||
|
html.classList.toggle('dark');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||||||
if (storedTheme !== 'system') return;
|
if (storedTheme !== 'system') return;
|
||||||
setDisplayedTheme(e.matches ? 'light' : 'dark');
|
setDisplayedTheme(e.matches ? 'light' : 'dark');
|
||||||
|
toggleHtml();
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
if (storedTheme === 'system') {
|
if (storedTheme === 'system') {
|
||||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
setDisplayedTheme(isBrowserThemeLight ? 'light' : 'dark');
|
setDisplayedTheme(isBrowserThemeLight ? 'light' : 'dark');
|
||||||
|
root.classList.add(isBrowserThemeLight ? 'light' : 'dark')
|
||||||
} else {
|
} else {
|
||||||
setDisplayedTheme(storedTheme);
|
setDisplayedTheme(storedTheme);
|
||||||
|
root.classList.add(storedTheme)
|
||||||
}
|
}
|
||||||
}, [storedTheme, setDisplayedTheme, window.matchMedia]);
|
}, [storedTheme, setDisplayedTheme, window.matchMedia]);
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import fileDialog from 'file-dialog';
|
|||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import { BrunoError } from 'utils/common/error';
|
import { BrunoError } from 'utils/common/error';
|
||||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||||
|
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||||
|
|
||||||
const readFile = (files) => {
|
const readFile = (files) => {
|
||||||
return new Promise((resolve, reject) => {
|
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 || [];
|
brunoParent.items = brunoParent.items || [];
|
||||||
const folderMap = {};
|
const folderMap = {};
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
|||||||
brunoParent.items.push(brunoFolderItem);
|
brunoParent.items.push(brunoFolderItem);
|
||||||
folderMap[folderName] = brunoFolderItem;
|
folderMap[folderName] = brunoFolderItem;
|
||||||
if (i.item && i.item.length) {
|
if (i.item && i.item.length) {
|
||||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (i.request) {
|
if (i.request) {
|
||||||
@ -121,9 +122,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
|||||||
brunoRequestItem.request.script = {};
|
brunoRequestItem.request.script = {};
|
||||||
}
|
}
|
||||||
if (Array.isArray(event.script.exec)) {
|
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 {
|
} 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) {
|
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||||
@ -131,9 +136,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
|||||||
brunoRequestItem.request.tests = {};
|
brunoRequestItem.request.tests = {};
|
||||||
}
|
}
|
||||||
if (Array.isArray(event.script.exec)) {
|
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 {
|
} 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;
|
return contentType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const importPostmanV2Collection = (collection) => {
|
const importPostmanV2Collection = (collection, options) => {
|
||||||
const brunoCollection = {
|
const brunoCollection = {
|
||||||
name: collection.info.name,
|
name: collection.info.name,
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@ -272,12 +281,12 @@ const importPostmanV2Collection = (collection) => {
|
|||||||
environments: []
|
environments: []
|
||||||
};
|
};
|
||||||
|
|
||||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
|
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
|
||||||
|
|
||||||
return brunoCollection;
|
return brunoCollection;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePostmanCollection = (str) => {
|
const parsePostmanCollection = (str, options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let collection = JSON.parse(str);
|
let collection = JSON.parse(str);
|
||||||
@ -289,7 +298,7 @@ const parsePostmanCollection = (str) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (v2Schemas.includes(schema)) {
|
if (v2Schemas.includes(schema)) {
|
||||||
return resolve(importPostmanV2Collection(collection));
|
return resolve(importPostmanV2Collection(collection, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BrunoError('Unknown postman schema');
|
throw new BrunoError('Unknown postman schema');
|
||||||
@ -304,11 +313,11 @@ const parsePostmanCollection = (str) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const importCollection = () => {
|
const importCollection = (options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fileDialog({ accept: 'application/json' })
|
fileDialog({ accept: 'application/json' })
|
||||||
.then(readFile)
|
.then(readFile)
|
||||||
.then(parsePostmanCollection)
|
.then((str) => parsePostmanCollection(str, options))
|
||||||
.then(transformItemsInCollection)
|
.then(transformItemsInCollection)
|
||||||
.then(hydrateSeqInCollection)
|
.then(hydrateSeqInCollection)
|
||||||
.then(validateSchema)
|
.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} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
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: {
|
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 {
|
try {
|
||||||
let collectionName = sanitizeDirectoryName(collection.name);
|
let collectionName = sanitizeDirectoryName(collection.name);
|
||||||
let collectionPath = path.join(collectionLocation, collectionName);
|
let collectionPath = path.join(collectionLocation, collectionName);
|
||||||
|
Loading…
Reference in New Issue
Block a user