mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
feat: add dropdown to select language and add lib selector in code gen (#4345)
* feat: add dropdown to select language and add lib selector in code gen * add: checkbox for interpolation * rm: url should interpolate from url * add: search in dropdown * fixes * add: autofocus for search * add: arrow navigation in select * fix code improvements fix rm: editor wrapper rm: font-size improvement rm: custom select rm comments and add sparql mode rm: styles * add: tests and fixes * fixes: file naming * rm: comments * fix * fix: unit tests * improvements * fixes * fix: indentation * fix * fixes: CodeViewToolbar * trim: extra spaces
This commit is contained in:
@@ -1,19 +1,59 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.editor-content {
|
||||
height: 100%;
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 0;
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background: ${props => props.theme.codemirror.gutter.bg};
|
||||
border-right: 1px solid ${props => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
font-size: 11px;
|
||||
padding: 0 3px 0 5px;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-to-clipboard {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
opacity: 0.5;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import get from 'lodash/get';
|
||||
import { HTTPSnippet } from 'httpsnippet';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconCopy } from '@tabler/icons';
|
||||
import { findCollectionByItemUid, getGlobalEnvironmentVariables } from '../../../../../../../utils/collections/index';
|
||||
import { getAuthHeaders } from '../../../../../../../utils/codegenerator/auth';
|
||||
import { findCollectionByItemUid, getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { generateSnippet } from '../utils/snippet-generator';
|
||||
|
||||
const CodeView = ({ language, item }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const { target, client, language: lang } = language;
|
||||
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
let _collection = findCollectionByItemUid(
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
|
||||
let collectionOriginal = findCollectionByItemUid(
|
||||
useSelector((state) => state.collections.collections),
|
||||
item.uid
|
||||
);
|
||||
|
||||
let collection = cloneDeep(_collection);
|
||||
const collection = useMemo(() => {
|
||||
const c = cloneDeep(collectionOriginal);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
c.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
return c;
|
||||
}, [collectionOriginal, globalEnvironments, activeGlobalEnvironmentUid]);
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const collectionRootAuth = collection?.root?.request?.auth;
|
||||
const requestAuth = item.draft ? get(item, 'draft.request.auth') : get(item, 'request.auth');
|
||||
|
||||
const headers = [
|
||||
...getAuthHeaders(collectionRootAuth, requestAuth),
|
||||
...(collection?.root?.request?.headers || []),
|
||||
...(requestHeaders || [])
|
||||
];
|
||||
|
||||
let snippet = '';
|
||||
try {
|
||||
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert(
|
||||
target,
|
||||
client
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snippet = 'Error generating code snippet';
|
||||
}
|
||||
const snippet = useMemo(() => {
|
||||
return generateSnippet({ language, item, collection, shouldInterpolate: generateCodePrefs.shouldInterpolate });
|
||||
}, [language, item, collection, generateCodePrefs.shouldInterpolate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper>
|
||||
<CopyToClipboard
|
||||
className="copy-to-clipboard"
|
||||
text={snippet}
|
||||
onCopy={() => toast.success('Copied to clipboard!')}
|
||||
>
|
||||
<StyledWrapper>
|
||||
<CopyToClipboard
|
||||
text={snippet}
|
||||
onCopy={() => toast.success('Copied to clipboard!')}
|
||||
>
|
||||
<button className="copy-to-clipboard">
|
||||
<IconCopy size={25} strokeWidth={1.5} />
|
||||
</CopyToClipboard>
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
<div className="editor-content">
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
@@ -67,10 +55,11 @@ const CodeView = ({ language, item }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
mode={lang}
|
||||
mode={language.language}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: ${props => props.theme.requestTabPanel.card.bg};
|
||||
border-bottom: 1px solid ${props => props.theme.requestTabPanel.card.border};
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.native-select {
|
||||
background: ${props => props.theme.requestTabPanel.url.bg};
|
||||
border: 1px solid ${props => props.theme.input.border};
|
||||
border-radius: 3px;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 12px;
|
||||
padding: 6px 28px 6px 10px;
|
||||
min-width: 140px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
appearance: none;
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
box-shadow: 0 0 0 2px ${props => props.theme.input.focusBoxShadow};
|
||||
}
|
||||
|
||||
option {
|
||||
background: ${props => props.theme.bg};
|
||||
color: ${props => props.theme.text};
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.library-options {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lib-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
background: ${props => props.theme.requestTabPanel.url.bg};
|
||||
border: 1px solid ${props => props.theme.input.border};
|
||||
border-radius: 3px;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.theme.dropdown.hoverBg};
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${props => props.theme.button.secondary.bg};
|
||||
border-color: ${props => props.theme.button.secondary.border};
|
||||
color: ${props => props.theme.button.secondary.color};
|
||||
}
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
.interpolate-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: ${props => props.theme.text};
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { updateGenerateCode } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CodeViewToolbar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const languages = getLanguages();
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
|
||||
// Group languages by their main language type
|
||||
const languageGroups = useMemo(() => {
|
||||
return languages.reduce((acc, lang) => {
|
||||
const mainLang = lang.name.split('-')[0];
|
||||
if (!acc[mainLang]) {
|
||||
acc[mainLang] = [];
|
||||
}
|
||||
acc[mainLang].push({
|
||||
...lang,
|
||||
libraryName: lang.name.split('-')[1] || 'default'
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}, [languages]);
|
||||
|
||||
const mainLanguages = useMemo(() => Object.keys(languageGroups), [languageGroups]);
|
||||
|
||||
const availableLibraries = useMemo(() => {
|
||||
return languageGroups[generateCodePrefs.mainLanguage] || [];
|
||||
}, [generateCodePrefs.mainLanguage, languageGroups]);
|
||||
|
||||
// Event handlers
|
||||
const handleMainLanguageChange = (e) => {
|
||||
const newMainLang = e.target.value;
|
||||
const defaultLibrary = languageGroups[newMainLang][0].libraryName;
|
||||
|
||||
dispatch(updateGenerateCode({
|
||||
mainLanguage: newMainLang,
|
||||
library: defaultLibrary
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLibraryChange = (libraryName) => {
|
||||
dispatch(updateGenerateCode({
|
||||
library: libraryName
|
||||
}));
|
||||
};
|
||||
|
||||
const handleInterpolateChange = (e) => {
|
||||
dispatch(updateGenerateCode({
|
||||
shouldInterpolate: e.target.checked
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="toolbar">
|
||||
<div className="left-controls">
|
||||
<div className="select-wrapper">
|
||||
<select
|
||||
className="native-select"
|
||||
value={generateCodePrefs.mainLanguage}
|
||||
onChange={handleMainLanguageChange}
|
||||
>
|
||||
{mainLanguages.map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<IconChevronDown size={16} className="select-arrow" />
|
||||
</div>
|
||||
|
||||
{availableLibraries.length > 1 && (
|
||||
<div className="library-options">
|
||||
{availableLibraries.map((lib) => (
|
||||
<button
|
||||
key={lib.libraryName}
|
||||
className={`lib-btn ${generateCodePrefs.library === lib.libraryName ? 'active' : ''}`}
|
||||
onClick={() => handleLibraryChange(lib.libraryName)}
|
||||
>
|
||||
{lib.libraryName}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="right-controls">
|
||||
<label className="interpolate-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={generateCodePrefs.shouldInterpolate}
|
||||
onChange={handleInterpolateChange}
|
||||
/>
|
||||
<span>Interpolate Variables</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeViewToolbar;
|
||||
@@ -1,60 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
margin: -1.5rem -1rem;
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.generate-code-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
max-height: 80vh;
|
||||
.code-generator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.generate-code-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
background: ${props => props.theme.bg};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
.error-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flexible-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.flexible-container {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 601px) and (max-width: 1200px) {
|
||||
.flexible-container {
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1201px) {
|
||||
.flexible-container {
|
||||
width: 900px;
|
||||
p {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,72 +1,30 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import CodeView from './CodeView';
|
||||
import CodeViewToolbar from './CodeViewToolbar';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { get } from 'lodash';
|
||||
import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections';
|
||||
import {
|
||||
findEnvironmentInCollection
|
||||
} from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Function to resolve inherited auth
|
||||
const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return {
|
||||
...request
|
||||
};
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let source = 'collection';
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
source = 'folder';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
import { resolveInheritedAuth } from './utils/auth-utils';
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
const environment = findEnvironmentInCollection(collection, collection?.activeEnvironmentUid);
|
||||
|
||||
let envVars = {};
|
||||
if (environment) {
|
||||
const vars = get(environment, 'variables', []);
|
||||
@@ -79,7 +37,6 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const requestUrl =
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
// interpolate the url
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
globalEnvironmentVariables,
|
||||
@@ -94,54 +51,27 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
|
||||
// Get the full language object based on current preferences
|
||||
const selectedLanguage = useMemo(() => {
|
||||
const fullName = generateCodePrefs.library === 'default'
|
||||
? generateCodePrefs.mainLanguage
|
||||
: `${generateCodePrefs.mainLanguage}-${generateCodePrefs.library}`;
|
||||
|
||||
return languages.find(lang => lang.name === fullName) || languages[0];
|
||||
}, [generateCodePrefs.mainLanguage, generateCodePrefs.library, languages]);
|
||||
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
<StyledWrapper>
|
||||
<div className="flex w-full flexible-container">
|
||||
<div>
|
||||
<div className="generate-code-sidebar">
|
||||
{languages &&
|
||||
languages.length &&
|
||||
languages.map((language) => (
|
||||
<div
|
||||
key={language.name}
|
||||
className={
|
||||
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedLanguage(language)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab' || (e.shiftKey && e.key === 'Tab')) {
|
||||
e.preventDefault();
|
||||
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
|
||||
const nextIndex = e.shiftKey
|
||||
? (currentIndex - 1 + languages.length) % languages.length
|
||||
: (currentIndex + 1) % languages.length;
|
||||
setSelectedLanguage(languages[nextIndex]);
|
||||
<div className="code-generator">
|
||||
<CodeViewToolbar />
|
||||
|
||||
// Explicitly focus on the new active element
|
||||
const nextElement = document.querySelector(`[data-language="${languages[nextIndex].name}"]`);
|
||||
nextElement?.focus();
|
||||
}
|
||||
|
||||
}}
|
||||
data-language={language.name}
|
||||
aria-pressed={language.name === selectedLanguage.name}
|
||||
>
|
||||
<span className="capitalize">{language.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow p-4">
|
||||
<div className="editor-container">
|
||||
{isValidUrl(finalUrl) ? (
|
||||
<CodeView
|
||||
tabIndex={-1}
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
@@ -152,11 +82,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center w-full">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">Invalid URL: {finalUrl}</h1>
|
||||
<p className="text-gray-500">Please check the URL and try again</p>
|
||||
</div>
|
||||
<div className="error-message">
|
||||
<h1>Invalid URL: {finalUrl}</h1>
|
||||
<p>Please check the URL and try again</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
findItemInCollection,
|
||||
findParentItemInCollection
|
||||
} from 'utils/collections';
|
||||
|
||||
export const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Resolve inherited auth by traversing up the folder hierarchy
|
||||
export const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return request;
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { resolveInheritedAuth } from './auth-utils';
|
||||
|
||||
// Helper to build mock collection structure
|
||||
const buildCollection = () => {
|
||||
return {
|
||||
uid: 'c1',
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'f1',
|
||||
type: 'folder',
|
||||
name: 'Folder',
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'r1',
|
||||
type: 'request',
|
||||
name: 'Request',
|
||||
request: {
|
||||
auth: { mode: 'inherit' },
|
||||
url: 'http://example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
describe('auth-utils.resolveInheritedAuth', () => {
|
||||
it('should resolve to nearest folder auth when request mode is inherit', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0]; // r1
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('basic');
|
||||
expect(resolved.auth.basic.username).toBe('user');
|
||||
});
|
||||
|
||||
it('should resolve to collection auth if no folder auth', () => {
|
||||
const collection = buildCollection();
|
||||
collection.items[0].root.request.auth = { mode: 'inherit' };
|
||||
const item = collection.items[0].items[0];
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('bearer');
|
||||
expect(resolved.auth.bearer.token).toBe('COLLECTION');
|
||||
});
|
||||
|
||||
it('should return original request when mode is not inherit', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0];
|
||||
item.request.auth = { mode: 'basic', basic: { username: 'override', password: 'pwd' } };
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('basic');
|
||||
expect(resolved.auth.basic.username).toBe('override');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export const interpolateHeaders = (headers = [], variables = {}) => {
|
||||
return headers.map((header) => ({
|
||||
...header,
|
||||
name: interpolate(header.name, variables),
|
||||
value: interpolate(header.value, variables)
|
||||
}));
|
||||
};
|
||||
|
||||
export const interpolateBody = (body, variables = {}) => {
|
||||
if (!body) return null;
|
||||
|
||||
const interpolatedBody = cloneDeep(body);
|
||||
|
||||
switch (body.mode) {
|
||||
case 'json':
|
||||
let parsed = body.json;
|
||||
// If it's already a string, use it directly; if it's an object, stringify it first
|
||||
if (typeof parsed === 'object') {
|
||||
parsed = JSON.stringify(parsed);
|
||||
}
|
||||
parsed = interpolate(parsed, variables, { escapeJSONStrings: true });
|
||||
try {
|
||||
const jsonObj = JSON.parse(parsed);
|
||||
interpolatedBody.json = JSON.stringify(jsonObj, null, 2);
|
||||
} catch {
|
||||
interpolatedBody.json = parsed;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
interpolatedBody.text = interpolate(body.text, variables);
|
||||
break;
|
||||
|
||||
case 'xml':
|
||||
interpolatedBody.xml = interpolate(body.xml, variables);
|
||||
break;
|
||||
|
||||
case 'sparql':
|
||||
interpolatedBody.sparql = interpolate(body.sparql, variables);
|
||||
break;
|
||||
|
||||
case 'formUrlEncoded':
|
||||
interpolatedBody.formUrlEncoded = body.formUrlEncoded.map((param) => ({
|
||||
...param,
|
||||
value: param.enabled ? interpolate(param.value, variables) : param.value
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'multipartForm':
|
||||
interpolatedBody.multipartForm = body.multipartForm.map((param) => ({
|
||||
...param,
|
||||
value:
|
||||
param.type === 'text' && param.enabled
|
||||
? interpolate(param.value, variables)
|
||||
: param.value
|
||||
}));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return interpolatedBody;
|
||||
};
|
||||
|
||||
export const createVariablesObject = ({
|
||||
globalEnvironmentVariables = {},
|
||||
collectionVars = {},
|
||||
allVariables = {},
|
||||
collection = {},
|
||||
runtimeVariables = {},
|
||||
processEnvVars = {}
|
||||
}) => {
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...allVariables,
|
||||
...collectionVars,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
|
||||
describe('interpolation utils', () => {
|
||||
describe('interpolateHeaders', () => {
|
||||
it('should interpolate variables in header name and value while preserving other props', () => {
|
||||
const headers = [
|
||||
{ uid: '1', name: 'X-{{var}}', value: 'value-{{var}}', enabled: true }
|
||||
];
|
||||
const variables = { var: 'test' };
|
||||
|
||||
const result = interpolateHeaders(headers, variables);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
uid: '1',
|
||||
name: 'X-test',
|
||||
value: 'value-test',
|
||||
enabled: true
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolateBody', () => {
|
||||
it('should interpolate JSON body strings and keep formatting', () => {
|
||||
const body = {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{username}}"}'
|
||||
};
|
||||
const variables = { username: 'bruno' };
|
||||
|
||||
const result = interpolateBody(body, variables);
|
||||
expect(result.json).toBe('{\n "name": "bruno"\n}');
|
||||
});
|
||||
|
||||
it('should interpolate text body', () => {
|
||||
const body = {
|
||||
mode: 'text',
|
||||
text: 'Hello {{name}}'
|
||||
};
|
||||
const result = interpolateBody(body, { name: 'World' });
|
||||
expect(result.text).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should return null when body is null', () => {
|
||||
expect(interpolateBody(null, { a: 1 })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
|
||||
import { resolveInheritedAuth } from './auth-utils';
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
try {
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
const { HTTPSnippet } = require('httpsnippet');
|
||||
|
||||
const allVariables = getAllVariables(collection, item);
|
||||
|
||||
// Create variables object for interpolation
|
||||
const variables = createVariablesObject({
|
||||
globalEnvironmentVariables: collection.globalEnvironmentVariables || {},
|
||||
collectionVars: collection.collectionVars || {},
|
||||
allVariables,
|
||||
collection,
|
||||
runtimeVariables: collection.runtimeVariables || {},
|
||||
processEnvVars: collection.processEnvVariables || {}
|
||||
});
|
||||
|
||||
// Get the request with resolved auth
|
||||
const request = resolveInheritedAuth(item, collection);
|
||||
|
||||
// Prepare headers
|
||||
let headers = [...(request.headers || [])];
|
||||
|
||||
// Add auth headers if needed
|
||||
if (request.auth && request.auth.mode !== 'none') {
|
||||
const authHeaders = getAuthHeaders(request.auth, variables);
|
||||
headers = [...headers, ...authHeaders];
|
||||
}
|
||||
|
||||
// Interpolate headers and body if needed
|
||||
if (shouldInterpolate) {
|
||||
headers = interpolateHeaders(headers, variables);
|
||||
if (request.body) {
|
||||
request.body = interpolateBody(request.body, variables);
|
||||
}
|
||||
}
|
||||
|
||||
// Build HAR request
|
||||
const harRequest = buildHarRequest({
|
||||
request,
|
||||
headers
|
||||
});
|
||||
|
||||
// Generate snippet using HTTPSnippet
|
||||
const snippet = new HTTPSnippet(harRequest);
|
||||
const result = snippet.convert(language.target, language.client);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error generating code snippet:', error);
|
||||
return 'Error generating code snippet';
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
generateSnippet
|
||||
};
|
||||
@@ -0,0 +1,421 @@
|
||||
jest.mock('httpsnippet', () => {
|
||||
return {
|
||||
HTTPSnippet: jest.fn().mockImplementation((harRequest) => ({
|
||||
convert: jest.fn(() => {
|
||||
const method = harRequest?.method || 'GET';
|
||||
const url = harRequest?.url || 'http://example.com';
|
||||
const hasBody = harRequest?.postData?.text;
|
||||
|
||||
if (method === 'POST' && hasBody) {
|
||||
return `curl -X POST ${url} -H "Content-Type: application/json" -d '${hasBody}'`;
|
||||
}
|
||||
return `curl -X ${method} ${url}`;
|
||||
})
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('utils/codegenerator/har', () => ({
|
||||
buildHarRequest: jest.fn((data) => {
|
||||
const request = data.request || {};
|
||||
const method = request.method || 'GET';
|
||||
const url = request.url || 'http://example.com';
|
||||
const body = request.body || {};
|
||||
|
||||
const harRequest = {
|
||||
method: method,
|
||||
url: url,
|
||||
headers: data.headers || [],
|
||||
httpVersion: 'HTTP/1.1'
|
||||
};
|
||||
|
||||
// Add body data for POST requests
|
||||
if (method === 'POST' && body.mode === 'json' && body.json) {
|
||||
harRequest.postData = {
|
||||
mimeType: 'application/json',
|
||||
text: body.json
|
||||
};
|
||||
}
|
||||
|
||||
return harRequest;
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('utils/codegenerator/auth', () => ({
|
||||
getAuthHeaders: jest.fn(() => [])
|
||||
}));
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getAllVariables: jest.fn(() => ({
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
}))
|
||||
}));
|
||||
|
||||
import { generateSnippet } from './snippet-generator';
|
||||
|
||||
describe('Snippet Generator - Simple Tests', () => {
|
||||
|
||||
// Simple test request - easy to understand
|
||||
const testRequest = {
|
||||
uid: 'test-request-123',
|
||||
name: 'test api call',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{apiToken}}', enabled: true },
|
||||
{ uid: 'h2', name: 'Content-Type', value: 'application/json', enabled: true },
|
||||
{ uid: 'h3', name: 'X-Custom', value: '{{customValue}}', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"message": "{{greeting}}", "count": {{number}}}'
|
||||
},
|
||||
auth: { mode: 'none' },
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: '',
|
||||
params: [],
|
||||
vars: { req: [] }
|
||||
}
|
||||
};
|
||||
|
||||
const testCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'data',
|
||||
apiToken: 'token123',
|
||||
customValue: 'test-value',
|
||||
greeting: 'Hello World',
|
||||
number: 42
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const curlLanguage = { target: 'shell', client: 'curl' };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({
|
||||
convert: jest.fn(() => {
|
||||
const method = harRequest?.method || 'GET';
|
||||
const url = harRequest?.url || 'http://example.com';
|
||||
const hasBody = harRequest?.postData?.text;
|
||||
|
||||
if (method === 'POST' && hasBody) {
|
||||
return `curl -X POST ${url} -H "Content-Type: application/json" -d '${hasBody}'`;
|
||||
}
|
||||
return `curl -X ${method} ${url}`;
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
it('should generate curl for POST request with JSON body', () => {
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"message": "{{greeting}}", "count": {{number}}}\'');
|
||||
});
|
||||
|
||||
it('should interpolate variables when enabled', () => {
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle GET requests', () => {
|
||||
const getRequest = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
method: 'GET',
|
||||
body: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: getRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X GET https://api.example.com/{{endpoint}}');
|
||||
});
|
||||
|
||||
it('should handle requests with different headers', () => {
|
||||
const requestWithDifferentHeaders = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'X-API-Key', value: '{{apiKey}}', enabled: true },
|
||||
{ uid: 'h2', name: 'Accept', value: 'application/json', enabled: true },
|
||||
{ uid: 'h3', name: 'User-Agent', value: 'TestApp/{{version}}', enabled: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const collectionWithDifferentVars = {
|
||||
...testCollection,
|
||||
globalEnvironmentVariables: {
|
||||
...testCollection.globalEnvironmentVariables,
|
||||
apiKey: 'secret-key-456',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: requestWithDifferentHeaders,
|
||||
collection: collectionWithDifferentVars,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
// Body should have interpolated variables with proper formatting
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle complex nested JSON body', () => {
|
||||
const complexBody = {
|
||||
user: {
|
||||
name: '{{userName}}',
|
||||
settings: {
|
||||
theme: '{{userTheme}}',
|
||||
active: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
items: ['{{item1}}', '{{item2}}'],
|
||||
total: '{{totalCount}}'
|
||||
}
|
||||
};
|
||||
|
||||
const requestWithComplexBody = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: JSON.stringify(complexBody, null, 2)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collectionWithComplexVars = {
|
||||
...testCollection,
|
||||
globalEnvironmentVariables: {
|
||||
...testCollection.globalEnvironmentVariables,
|
||||
userName: 'Alice',
|
||||
userTheme: 'dark',
|
||||
item1: 'first',
|
||||
item2: 'second',
|
||||
totalCount: 100
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: requestWithComplexBody,
|
||||
collection: collectionWithComplexVars,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedComplexBody = JSON.stringify({
|
||||
user: {
|
||||
name: 'Alice',
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
active: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
items: ['first', 'second'],
|
||||
total: '100'
|
||||
}
|
||||
}, null, 2);
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
// Set up the error mock after beforeEach has run
|
||||
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
|
||||
require('httpsnippet').HTTPSnippet = jest.fn(() => {
|
||||
throw new Error('Mock error!');
|
||||
});
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('Error generating code snippet');
|
||||
|
||||
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('should work with JavaScript language', () => {
|
||||
const javascriptLanguage = { target: 'javascript', client: 'fetch' };
|
||||
|
||||
const expectedJavaScriptCode = `fetch("https://api.example.com/data", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ "message": "Hello World", "count": 42 })
|
||||
})`;
|
||||
|
||||
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
|
||||
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation(() => ({
|
||||
convert: jest.fn(() => expectedJavaScriptCode)
|
||||
}));
|
||||
|
||||
const result = generateSnippet({
|
||||
language: javascriptLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe(expectedJavaScriptCode);
|
||||
|
||||
// Restore the original mock
|
||||
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
|
||||
});
|
||||
|
||||
it('should interpolate simple headers and body variables', () => {
|
||||
const simpleTestRequest = {
|
||||
uid: 'test-123',
|
||||
name: 'simple test',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.test.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },
|
||||
{ uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },
|
||||
{ uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Simple collection with clear variable values
|
||||
const simpleTestCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'users',
|
||||
token: 'abc123token',
|
||||
userId: 'user456',
|
||||
userName: 'John Smith',
|
||||
userEmail: 'john@test.com',
|
||||
userAge: 30
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: simpleTestRequest,
|
||||
collection: simpleTestCollection,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedInterpolatedBody = `{
|
||||
"name": "John Smith",
|
||||
"email": "john@test.com",
|
||||
"age": 30
|
||||
}`;
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
|
||||
});
|
||||
|
||||
it('should NOT interpolate when shouldInterpolate is false', () => {
|
||||
const simpleTestRequest = {
|
||||
uid: 'test-123',
|
||||
name: 'simple test',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.test.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },
|
||||
{ uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },
|
||||
{ uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const simpleTestCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'users',
|
||||
token: 'abc123token',
|
||||
userId: 'user456',
|
||||
userName: 'John Smith',
|
||||
userEmail: 'john@test.com',
|
||||
userAge: 30
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: simpleTestRequest,
|
||||
collection: simpleTestCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,11 @@ const initialState = {
|
||||
codeFont: 'default'
|
||||
}
|
||||
},
|
||||
generateCode: {
|
||||
mainLanguage: 'Shell',
|
||||
library: 'curl',
|
||||
shouldInterpolate: true
|
||||
},
|
||||
cookies: [],
|
||||
taskQueue: [],
|
||||
systemProxyEnvVariables: {}
|
||||
@@ -75,6 +80,12 @@ export const appSlice = createSlice({
|
||||
},
|
||||
updateSystemProxyEnvVariables: (state, action) => {
|
||||
state.systemProxyEnvVariables = action.payload;
|
||||
},
|
||||
updateGenerateCode: (state, action) => {
|
||||
state.generateCode = {
|
||||
...state.generateCode,
|
||||
...action.payload
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -93,7 +104,8 @@ export const {
|
||||
insertTaskIntoQueue,
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue,
|
||||
updateSystemProxyEnvVariables
|
||||
updateSystemProxyEnvVariables,
|
||||
updateGenerateCode
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
|
||||
Reference in New Issue
Block a user