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:
Baptiste Poulain 2024-03-13 14:10:31 +01:00 committed by GitHub
parent 2cd0e065bd
commit 410eecc884
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 155 additions and 36 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -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}>
Bruno Collection
<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
</CollectionButton>
<CollectionButton onClick={handleImportPostmanCollection}>
Postman Collection
</CollectionButton>
<CollectionButton onClick={handleImportInsomniaCollection}>
Insomnia Collection
</CollectionButton>
<CollectionButton onClick={handleImportOpenapiCollection}>
OpenAPI V3 Spec
</CollectionButton>
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportPostmanCollection}>
Postman Collection
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
Insomnia Collection
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
OpenAPI V3 Spec
<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>

View File

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

View File

@ -26,7 +26,7 @@ const TitleBar = () => {
setImportCollectionLocationModalOpen(true);
};
const handleImportCollectionLocation = (collectionLocation) => {
const handleImportCollectionLocation = (collectionLocation, useTranslation) => {
dispatch(importCollection(importedCollection, collectionLocation));
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
},
},
extend: {},
},
plugins: []
};
plugins: [require("@tailwindcss/forms")],
}

View File

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