mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 21:25:45 +00:00
Merge branch 'main' into folder_sequencing_cli
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -46,4 +46,5 @@ yarn-error.log*
|
||||
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
.idea
|
||||
.vscode
|
||||
3
.husky/pre-commit
Executable file
3
.husky/pre-commit
Executable file
@@ -0,0 +1,3 @@
|
||||
# .husky/pre-commit
|
||||
|
||||
npx lint-staged
|
||||
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
2365
package-lock.json
generated
2365
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,12 @@
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
@@ -59,7 +62,8 @@
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"lint": "npx eslint ./"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5",
|
||||
@@ -68,5 +72,8 @@
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"packages/**/*.{js,jsx,ts,tsx}": "npm run lint"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
@@ -34,6 +34,16 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
],
|
||||
// Add externals configuration to exclude Node.js libraries
|
||||
externals: {
|
||||
// List specific Node.js modules you want to exclude
|
||||
// Format: 'module-name': 'commonjs module-name'
|
||||
'worker_threads': 'commonjs worker_threads',
|
||||
// 'path': 'commonjs path'
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -13,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -160,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
|
||||
@@ -28,7 +28,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
|
||||
const folderRoot = folder?.root;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
|
||||
const hasTests = folderRoot?.request?.tests;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { find } from "lodash";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
|
||||
@@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const FolderNotFound = ({ folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = useCallback(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [folderUid]
|
||||
})
|
||||
);
|
||||
}, [dispatch, folderUid]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
if (!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<div>Folder no longer exists.</div>
|
||||
<div className="mt-2">
|
||||
This can happen when the folder was renamed or deleted on your filesystem.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
Close Tab
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderNotFound;
|
||||
@@ -25,7 +25,7 @@ import { produce } from 'immer';
|
||||
import CollectionOverview from 'components/CollectionSettings/Overview';
|
||||
import RequestNotLoaded from './RequestNotLoaded';
|
||||
import RequestIsLoading from './RequestIsLoading';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import FolderNotFound from './FolderNotFound';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -165,11 +165,7 @@ const RequestTabPanel = () => {
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
if (!folder) {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [activeTabUid]
|
||||
})
|
||||
);
|
||||
return <FolderNotFound folderUid={focusedTab.folderUid} />;
|
||||
}
|
||||
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
|
||||
@@ -76,7 +76,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{tab.type === 'folder-settings' ? (
|
||||
{tab.type === 'folder-settings' && !folder ? (
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
) : tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
|
||||
@@ -261,13 +263,14 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
return (
|
||||
<Fragment>
|
||||
{showAddNewRequestModal && (
|
||||
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
<NewRequest collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
)}
|
||||
|
||||
{showCloneRequestModal && (
|
||||
<CloneCollectionItem
|
||||
item={currentTabItem}
|
||||
collection={collection}
|
||||
collectionUid={collection.uid}
|
||||
collectionPathname={collection.pathname}
|
||||
onClose={() => setShowCloneRequestModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -79,7 +79,7 @@ const RequestTabs = () => {
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />
|
||||
<NewRequest collectionUid={activeCollection?.uid} collectionPathname={activeCollection?.pathname} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
|
||||
@@ -2,14 +2,20 @@ import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseSize = ({ size }) => {
|
||||
|
||||
if (!Number.isFinite(size)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sizeToDisplay = '';
|
||||
|
||||
// If size is greater than 1024 bytes, format as KB
|
||||
if (size > 1024) {
|
||||
// size is greater than 1kb
|
||||
let kb = Math.floor(size / 1024);
|
||||
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
|
||||
sizeToDisplay = kb + '.' + decimal + 'KB';
|
||||
} else {
|
||||
// If size is less than or equal to 1024 bytes, display as bytes (B)
|
||||
sizeToDisplay = size + 'B';
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
const responseSize = response.size || 0;
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -156,7 +157,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={response.size} />
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,11 @@ import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const ShareCollection = ({ onClose, collection }) => {
|
||||
const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const handleExportBrunoCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -11,11 +11,13 @@ import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
const CloneCollection = ({ onClose, collectionUid }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
values.collectionName,
|
||||
values.collectionFolderName,
|
||||
values.collectionLocation,
|
||||
collection.pathname
|
||||
collection?.pathname
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Request cloned!');
|
||||
onClose();
|
||||
@@ -172,8 +172,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
dirName={path.relative(collectionPathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.drag-preview {
|
||||
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
import {
|
||||
IconFile,
|
||||
IconFolder,
|
||||
} from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
function getItemStyles({ x, y }) {
|
||||
if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' };
|
||||
const transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
top: 0,
|
||||
transform,
|
||||
WebkitTransform: transform,
|
||||
zIndex: 100,
|
||||
};
|
||||
}
|
||||
|
||||
export const CollectionItemDragPreview = () => {
|
||||
const {
|
||||
item,
|
||||
isDragging,
|
||||
clientOffset
|
||||
} = useDragLayer((monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
isDragging: monitor.isDragging(),
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
}));
|
||||
if (!isDragging) return null;
|
||||
const { x, y } = clientOffset || {};
|
||||
const shouldShowFolderIcon = !item.type || item.type === 'folder';
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div style={getItemStyles({ x, y })} className='p-2'>
|
||||
<div className='flex items-center gap-2 border border-gray-500/10 rounded-md px-2 py-1 drag-preview'>
|
||||
{shouldShowFolderIcon ? (
|
||||
<IconFolder size={16} />
|
||||
) : (
|
||||
<IconFile size={16} />
|
||||
)}
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { recursivelyGetAllItemUids } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollectionItem = ({ onClose, item, collection }) => {
|
||||
const DeleteCollectionItem = ({ onClose, item, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
|
||||
dispatch(deleteItem(item.uid, collectionUid)).then(() => {
|
||||
|
||||
if (isFolder) {
|
||||
// close all tabs that belong to the folder
|
||||
|
||||
@@ -62,6 +62,7 @@ const CodeView = ({ language, item }) => {
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
item={item}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
|
||||
@@ -10,9 +10,11 @@ import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
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 });
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
return;
|
||||
}
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
await dispatch(saveRequest(item.uid, collectionUid, true));
|
||||
}
|
||||
const { name: newName, filename: newFilename } = values;
|
||||
try {
|
||||
let renameConfig = {
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid,
|
||||
};
|
||||
renameConfig['newName'] = newName;
|
||||
if (itemFilename !== newFilename) {
|
||||
@@ -191,8 +191,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
dirName={path.relative(collectionPathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,18 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
@@ -34,8 +36,6 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const recursiveRunLength = getRequestsCount(flattenedItems);
|
||||
|
||||
const isFolderLoading = areItemsLoading(item);
|
||||
console.log(item);
|
||||
console.log(isFolderLoading);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
.menu-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
@@ -22,6 +23,65 @@ const Wrapper = styled.div`
|
||||
height: 1.875rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
/* Common styles for drop indicators */
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.dragAndDrop.border};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Drop target styles */
|
||||
&.drop-target {
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inside drop target style */
|
||||
&.drop-target {
|
||||
&::before {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
background: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
// border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
transform: rotateZ(90deg);
|
||||
@@ -45,6 +105,20 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.item-target {
|
||||
background: #ccc3;
|
||||
}
|
||||
|
||||
&.item-seperator {
|
||||
.seperator {
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
background: #ccc3;
|
||||
}
|
||||
}
|
||||
|
||||
&.item-focused-in-tab {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
@@ -6,7 +7,7 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { handleCollectionItemDrop, moveItem, sendRequest, showInFolder, updateItemsSequences } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -26,13 +27,22 @@ import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
|
||||
import { isEqual } from 'lodash';
|
||||
import { calculateDraggedItemNewPathname } from 'utils/collections/index';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
|
||||
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
|
||||
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
|
||||
|
||||
const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid });
|
||||
const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual);
|
||||
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
const collectionItemRef = useRef(null);
|
||||
|
||||
// We use a single ref for drag and drop.
|
||||
const ref = useRef(null);
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
@@ -44,10 +54,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `collection-item-${collection.uid}`,
|
||||
item: item,
|
||||
const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag({
|
||||
type: `collection-item-${collectionUid}`,
|
||||
item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
}),
|
||||
@@ -56,21 +69,72 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `collection-item-${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
const determineDropType = (monitor) => {
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!hoverBoundingRect || !clientOffset) return null;
|
||||
|
||||
const clientY = clientOffset.y - hoverBoundingRect.top;
|
||||
const folderUpperThreshold = hoverBoundingRect.height * 0.35;
|
||||
const fileUpperThreshold = hoverBoundingRect.height * 0.5;
|
||||
|
||||
if (isItemAFolder(item)) {
|
||||
return clientY < folderUpperThreshold ? 'adjacent' : 'inside';
|
||||
} else {
|
||||
return clientY < fileUpperThreshold ? 'adjacent' : null;
|
||||
}
|
||||
};
|
||||
|
||||
const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => {
|
||||
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
|
||||
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return false;
|
||||
|
||||
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname });
|
||||
if (!newPathname) return false;
|
||||
|
||||
if (targetItemPathname?.startsWith(draggedItemPathname)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: `collection-item-${collectionUid}`,
|
||||
hover: (draggedItem, monitor) => {
|
||||
const { uid: targetItemUid } = item;
|
||||
const { uid: draggedItemUid } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
|
||||
const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType });
|
||||
|
||||
setDropType(_canItemBeDropped ? dropType : null);
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
drop: async (draggedItem, monitor) => {
|
||||
const { uid: targetItemUid } = item;
|
||||
const { uid: draggedItemUid } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
if (!dropType) return;
|
||||
|
||||
await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid }))
|
||||
setDropType(null);
|
||||
},
|
||||
canDrop: (draggedItem) => draggedItem.uid !== item.uid,
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
isOver: monitor.isOver()
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(collectionItemRef));
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -84,13 +148,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
'rotate-90': !itemIsCollapsed
|
||||
});
|
||||
|
||||
const itemRowClassName = classnames('flex collection-item-name items-center', {
|
||||
'item-focused-in-tab': item.uid == activeTabUid,
|
||||
'item-hovered': isOver
|
||||
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
|
||||
'item-focused-in-tab': isTabForItemActive,
|
||||
'item-hovered': isOver && canDrop,
|
||||
'drop-target': isOver && dropType === 'inside',
|
||||
'drop-target-above': isOver && dropType === 'adjacent'
|
||||
});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
dispatch(sendRequest(item, collectionUid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
@@ -101,12 +167,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
if (event && event.detail != 1) return;
|
||||
//scroll to the active tab
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
|
||||
const isRequest = isItemARequest(item);
|
||||
|
||||
if (isRequest) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
if (isTabForItemPresent) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
@@ -114,11 +178,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item),
|
||||
type: 'request',
|
||||
})
|
||||
@@ -127,14 +190,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
type: 'folder-settings',
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -146,10 +209,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
@@ -164,7 +227,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
let indents = range(item.depth);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
@@ -183,49 +245,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
dispatch(makeTabPermanent({ uid: item.uid }))
|
||||
dispatch(makeTabPermanent({ uid: item.uid }));
|
||||
};
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
// Sort items by their "seq" property.
|
||||
const sortItemsBySequence = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
const viewFolderSettings = () => {
|
||||
if (isItemAFolder(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowInFolder = () => {
|
||||
dispatch(showInFolder(item.pathname)).catch((error) => {
|
||||
console.error('Error opening the folder', error);
|
||||
@@ -233,62 +260,89 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i)));
|
||||
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i)));
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (
|
||||
(item?.request?.url !== '') ||
|
||||
(item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
|
||||
) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
const viewFolderSettings = () => {
|
||||
if (isItemAFolder(item)) {
|
||||
if (isTabForItemPresent) {
|
||||
dispatch(focusTab({ uid: item.uid }));
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
{renameItemModalOpen && (
|
||||
<RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)} />
|
||||
<RenameCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setRenameItemModalOpen(false)} />
|
||||
)}
|
||||
{cloneItemModalOpen && (
|
||||
<CloneCollectionItem item={item} collection={collection} onClose={() => setCloneItemModalOpen(false)} />
|
||||
<CloneCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setCloneItemModalOpen(false)} />
|
||||
)}
|
||||
{deleteItemModalOpen && (
|
||||
<DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />
|
||||
<DeleteCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setDeleteItemModalOpen(false)} />
|
||||
)}
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />
|
||||
<NewRequest item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
{newFolderModalOpen && (
|
||||
<NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />
|
||||
<NewFolder item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setNewFolderModalOpen(false)} />
|
||||
)}
|
||||
{runCollectionModalOpen && (
|
||||
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||
<RunCollectionItem collectionUid={collectionUid} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||
)}
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem collectionUid={collectionUid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
{itemInfoModalOpen && (
|
||||
<CollectionItemInfo item={item} collection={collection} onClose={() => setItemInfoModalOpen(false)} />
|
||||
<CollectionItemInfo item={item} onClose={() => setItemInfoModalOpen(false)} />
|
||||
)}
|
||||
<div className={itemRowClassName} ref={collectionItemRef}>
|
||||
<div
|
||||
className={itemRowClassName}
|
||||
ref={(node) => {
|
||||
ref.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{
|
||||
width: 16,
|
||||
minWidth: 16,
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
{/* Indent */}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
? indents.map((i) => (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{ width: 16, minWidth: 16, height: '100%' }}
|
||||
>
|
||||
{/* Indent */}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<div
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
}}
|
||||
style={{ paddingLeft: 8 }}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
@@ -304,10 +358,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="ml-1 flex w-full h-full items-center overflow-hidden"
|
||||
>
|
||||
<div className="ml-1 flex w-full h-full items-center overflow-hidden">
|
||||
<CollectionItemIcon item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
{item.name}
|
||||
@@ -429,17 +480,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!itemIsCollapsed ? (
|
||||
<div>
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
@@ -448,4 +498,4 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionItem;
|
||||
export default React.memo(CollectionItem);
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const RemoveCollection = ({ onClose, collection }) => {
|
||||
const RemoveCollection = ({ onClose, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeCollection(collection.uid))
|
||||
|
||||
@@ -2,13 +2,15 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { renameCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const RenameCollection = ({ collection, onClose }) => {
|
||||
const RenameCollection = ({ collectionUid, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
|
||||
@@ -13,7 +13,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&.item-hovered {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
border-bottom: 2px solid transparent;
|
||||
.collection-actions {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
@@ -62,6 +63,36 @@ const Wrapper = styled.div`
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target {
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
border: none;
|
||||
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
margin-top: -2px;
|
||||
background: transparent;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
border: none;
|
||||
border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
margin-bottom: -2px;
|
||||
background: transparent;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
}
|
||||
|
||||
.collection-name.drop-target {
|
||||
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
border-radius: 4px;
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
margin: -2px;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
}
|
||||
|
||||
#sidebar-collection-name {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
@@ -6,8 +7,8 @@ import { useDrop, useDrag } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -19,9 +20,10 @@ import { isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
import RenameCollection from './RenameCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CloneCollection from './CloneCollection';
|
||||
import { areItemsLoading, findItemInCollection } from 'utils/collections';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
@@ -33,7 +35,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
const collectionRef = useRef(null);
|
||||
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
@@ -127,8 +129,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
const isCollectionItem = (itemType) => {
|
||||
return itemType.startsWith('collection-item');
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag({
|
||||
type: "collection",
|
||||
item: collection,
|
||||
collect: (monitor) => ({
|
||||
@@ -144,7 +146,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
drop: (draggedItem, monitor) => {
|
||||
const itemType = monitor.getItemType();
|
||||
if (isCollectionItem(itemType)) {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
|
||||
dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid }))
|
||||
} else {
|
||||
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
|
||||
}
|
||||
@@ -157,7 +159,9 @@ const Collection = ({ collection, searchText }) => {
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(collectionRef));
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
@@ -170,36 +174,35 @@ const Collection = ({ collection, searchText }) => {
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
const sortItemsBySequence = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showNewRequestModal && <NewRequest collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && (
|
||||
<RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />
|
||||
<RenameCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowRenameCollectionModal(false)} />
|
||||
)}
|
||||
{showRemoveCollectionModal && (
|
||||
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
|
||||
<RemoveCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowRemoveCollectionModal(false)} />
|
||||
)}
|
||||
{showShareCollectionModal && (
|
||||
<ShareCollection collection={collection} onClose={() => setShowShareCollectionModal(false)} />
|
||||
<ShareCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowShareCollectionModal(false)} />
|
||||
)}
|
||||
{showCloneCollectionModalOpen && (
|
||||
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
<CloneCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
)}
|
||||
<CollectionItemDragPreview />
|
||||
<div className={collectionRowClassName}
|
||||
ref={collectionRef}
|
||||
ref={(node) => {
|
||||
collectionRef.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
@@ -296,20 +299,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{!collectionIsCollapsed ? (
|
||||
<div>
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{folderItems?.map?.((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
|
||||
})}
|
||||
{requestItems?.map?.((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import { postmanToBruno, readFile } from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import importOpenapiCollection from 'utils/importers/openapi-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import fileDialog from 'file-dialog';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'));
|
||||
.catch((err) => toastError(err, 'Import collection failed'))
|
||||
};
|
||||
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then((...args) => {
|
||||
setIsLoading(true);
|
||||
return readFile(...args);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => handleSubmit({ collection }))
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
|
||||
const handleImportInsomniaCollection = () => {
|
||||
importInsomniaCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'))
|
||||
};
|
||||
|
||||
const handleImportOpenapiCollection = () => {
|
||||
@@ -36,8 +45,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'))
|
||||
};
|
||||
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -50,18 +60,67 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
</button>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<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>
|
||||
|
||||
const FullscreenLoader = () => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
|
||||
// Cycle through loading messages for better UX
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
let messageIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % loadingMessages.length;
|
||||
setLoadingMessage(loadingMessages[messageIndex]);
|
||||
}, 2000);
|
||||
|
||||
setLoadingMessage(loadingMessages[0]);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center">
|
||||
<IconLoader2 className="animate-spin h-12 w-12 mb-4" strokeWidth={1.5} />
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2">
|
||||
{loadingMessage}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This may take a moment depending on the collection size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <FullscreenLoader />}
|
||||
{!isLoading && (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<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>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import Dropdown from "components/Dropdown";
|
||||
import { IconCaretDown } from "@tabler/icons";
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewFolder = ({ collection, item, onClose }) => {
|
||||
const NewFolder = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
@@ -52,7 +52,7 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
|
||||
dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null))
|
||||
.then(() => {
|
||||
toast.success('New folder created!');
|
||||
onClose();
|
||||
|
||||
@@ -5,7 +5,7 @@ import toast from 'react-hot-toast';
|
||||
import path from 'utils/common/path';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
@@ -20,9 +20,11 @@ import Portal from 'components/Portal';
|
||||
import Help from 'components/Help';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const {
|
||||
brunoConfig: { presets: collectionPresets = {} }
|
||||
} = collection;
|
||||
@@ -135,14 +137,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType })
|
||||
})
|
||||
);
|
||||
@@ -158,7 +160,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: curlRequestTypeDetected,
|
||||
requestUrl: request.url,
|
||||
requestMethod: request.method,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid: item ? item.uid : null,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
@@ -178,7 +180,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid: item ? item.uid : null
|
||||
})
|
||||
)
|
||||
@@ -389,8 +391,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, item?.pathname || collection?.pathname)}
|
||||
baseName={formik.values.filename? `${formik.values.filename}.bru` : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,12 +13,9 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollection,
|
||||
findParentItemInCollection,
|
||||
getItemsToResequence,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
isItemARequest,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
@@ -47,8 +44,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
|
||||
import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory, calculateDraggedItemNewPathname } from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
|
||||
|
||||
@@ -60,7 +56,7 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:rename-collection', newName, collection.pathname).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
@@ -337,6 +333,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
|
||||
})
|
||||
);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke(
|
||||
'renderer:run-collection-folder',
|
||||
@@ -358,6 +355,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
|
||||
export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection;
|
||||
const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
@@ -372,10 +371,27 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
if (!folderWithSameNameExists) {
|
||||
const fullName = path.join(collection.pathname, directoryName);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:new-folder', fullName, folderName)
|
||||
.then(() => resolve())
|
||||
.invoke('renderer:new-folder', fullName)
|
||||
.then(async () => {
|
||||
const folderData = {
|
||||
name: folderName,
|
||||
pathname: fullName,
|
||||
root: {
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
@@ -392,8 +408,26 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:new-folder', fullName, folderName)
|
||||
.then(() => resolve())
|
||||
.invoke('renderer:new-folder', fullName)
|
||||
.then(async () => {
|
||||
const folderData = {
|
||||
name: folderName,
|
||||
pathname: fullName,
|
||||
root: {
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
@@ -495,8 +529,11 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp
|
||||
set(item, 'name', newName);
|
||||
set(item, 'filename', newFilename);
|
||||
set(item, 'root.meta.name', newName);
|
||||
|
||||
set(item, 'root.meta.seq', parentFolder?.items?.length + 1);
|
||||
|
||||
const collectionPath = path.join(parentFolder.pathname, newFilename);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
@@ -594,176 +631,114 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
export const sortCollections = (payload) => (dispatch) => {
|
||||
dispatch(_sortCollections(payload));
|
||||
};
|
||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||
|
||||
export const moveItem = ({ targetDirname, sourcePathname }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:move-item', { targetDirname, sourcePathname })
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, collectionUid }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
|
||||
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
|
||||
const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection;
|
||||
const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items);
|
||||
const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection;
|
||||
const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items);
|
||||
|
||||
const handleMoveToNewLocation = async ({ draggedItem, draggedItemDirectoryItems, targetItem, targetItemDirectoryItems, newPathname, dropType }) => {
|
||||
const { uid: targetItemUid } = targetItem;
|
||||
const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem;
|
||||
|
||||
const newDirname = path.dirname(newPathname);
|
||||
await dispatch(moveItem({
|
||||
targetDirname: newDirname,
|
||||
sourcePathname: draggedItemPathname
|
||||
}));
|
||||
|
||||
// Update sequences in the source directory
|
||||
if (draggedItemDirectoryItems?.length) {
|
||||
// reorder items in the source directory
|
||||
const draggedItemDirectoryItemsWithoutDraggedItem = draggedItemDirectoryItems.filter(i => i.uid !== draggedItemUid);
|
||||
const reorderedSourceItems = getReorderedItemsInSourceDirectory({ items: draggedItemDirectoryItemsWithoutDraggedItem });
|
||||
if (reorderedSourceItems?.length) {
|
||||
await dispatch(updateItemsSequences({ itemsToResequence: reorderedSourceItems }));
|
||||
}
|
||||
}
|
||||
|
||||
// Update sequences in the target directory (if dropping adjacent)
|
||||
if (dropType === 'adjacent') {
|
||||
const targetItemSequence = targetItemDirectoryItems.findIndex(i => i.uid === targetItemUid)?.seq;
|
||||
|
||||
const draggedItemWithNewPathAndSequence = {
|
||||
...draggedItem,
|
||||
pathname: newPathname,
|
||||
seq: targetItemSequence
|
||||
};
|
||||
|
||||
// draggedItem is added to the targetItem's directory
|
||||
const reorderedTargetItems = getReorderedItemsInTargetDirectory({
|
||||
items: [ ...targetItemDirectoryItems, draggedItemWithNewPathAndSequence ],
|
||||
targetItemUid,
|
||||
draggedItemUid
|
||||
});
|
||||
|
||||
if (reorderedTargetItems?.length) {
|
||||
await dispatch(updateItemsSequences({ itemsToResequence: reorderedTargetItems }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderInSameLocation = async ({ draggedItem, targetItem, targetItemDirectoryItems }) => {
|
||||
const { uid: targetItemUid } = targetItem;
|
||||
const { uid: draggedItemUid } = draggedItem;
|
||||
|
||||
// reorder items in the targetItem's directory
|
||||
const reorderedItems = getReorderedItemsInTargetDirectory({
|
||||
items: targetItemDirectoryItems,
|
||||
targetItemUid,
|
||||
draggedItemUid
|
||||
});
|
||||
|
||||
if (reorderedItems?.length) {
|
||||
await dispatch(updateItemsSequences({ itemsToResequence: reorderedItems }));
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname: collection.pathname });
|
||||
if (!newPathname) return;
|
||||
if (targetItemPathname?.startsWith(draggedItemPathname)) return;
|
||||
if (newPathname !== draggedItemPathname) {
|
||||
await handleMoveToNewLocation({ targetItem, targetItemDirectoryItems, draggedItem, draggedItemDirectoryItems, newPathname, dropType });
|
||||
} else {
|
||||
await handleReorderInSameLocation({ draggedItem, targetItemDirectoryItems, targetItem });
|
||||
}
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(error?.message);
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const updateItemsSequences = ({ itemsToResequence }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
if (!targetItem) {
|
||||
return reject(new Error('Target item not found'));
|
||||
}
|
||||
|
||||
const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid);
|
||||
const targetItemParent = findParentItemInCollection(collectionCopy, targetItemUid);
|
||||
const sameParent = draggedItemParent === targetItemParent;
|
||||
|
||||
// file item dragged onto another file item and both are in the same folder
|
||||
// this is also true when both items are at the root level
|
||||
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && sameParent) {
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:resequence-items', itemsToResequence)
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
// file item dragged onto another file item which is at the root level
|
||||
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !targetItemParent) {
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
// file item dragged onto another file item and both are in different folders
|
||||
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !sameParent) {
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, targetItemParent.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
// file item dragged into its own folder
|
||||
if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// file item dragged into another folder
|
||||
if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) {
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(targetItem, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, targetItem.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
// end of the file drags, now let's handle folder drags
|
||||
// folder drags are simpler since we don't allow ordering of folders
|
||||
|
||||
// folder dragged into its own folder
|
||||
if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// folder dragged into a file which is at the same level
|
||||
// this is also true when both items are at the root level
|
||||
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && sameParent) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// folder dragged into a file which is a child of the folder
|
||||
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && draggedItem === targetItemParent) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// folder dragged into a file which is at the root level
|
||||
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && !targetItemParent) {
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
// folder dragged into another folder
|
||||
if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) {
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-folder-item', draggedItemPathname, targetItem.pathname)
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid);
|
||||
// file item is already at the root level
|
||||
if (!draggedItemParent) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const draggedItemPathname = draggedItem.pathname;
|
||||
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
|
||||
|
||||
if (isItemAFolder(draggedItem)) {
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||
const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy);
|
||||
|
||||
return ipcRenderer
|
||||
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||
.then(resolve)
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
|
||||
@@ -823,8 +798,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
collection.items,
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
|
||||
);
|
||||
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));
|
||||
item.seq = items.length + 1;
|
||||
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = path.join(collection.pathname, resolvedFilename);
|
||||
@@ -852,8 +827,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
currentItem.items,
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
|
||||
);
|
||||
const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
const items = filter(currentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));
|
||||
item.seq = items.length + 1;
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = path.join(currentItem.pathname, resolvedFilename);
|
||||
const { ipcRenderer } = window;
|
||||
@@ -885,6 +860,7 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name)
|
||||
.then(
|
||||
@@ -913,6 +889,7 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch,
|
||||
|
||||
const sanitizedName = sanitizeName(name);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, variables)
|
||||
.then(
|
||||
@@ -946,6 +923,7 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
|
||||
|
||||
const sanitizedName = sanitizeName(name);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables)
|
||||
.then(
|
||||
@@ -982,6 +960,7 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di
|
||||
const oldName = environment.name;
|
||||
environment.name = sanitizedName;
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName))
|
||||
@@ -1005,6 +984,7 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, g
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-environment', collection.pathname, environment.name)
|
||||
.then(resolve)
|
||||
@@ -1028,6 +1008,7 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
|
||||
|
||||
environment.variables = variables;
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment))
|
||||
@@ -1053,7 +1034,8 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g
|
||||
if (environmentUid && !environmentName) {
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }});
|
||||
|
||||
dispatch(_selectEnvironment({ environmentUid, collectionUid }));
|
||||
@@ -1112,11 +1094,13 @@ export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getS
|
||||
const state = getState();
|
||||
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid)
|
||||
.then(resolve)
|
||||
@@ -1135,6 +1119,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
|
||||
collectionSchema
|
||||
|
||||
@@ -1719,6 +1719,9 @@ export const collectionsSlice = createSlice({
|
||||
folderItem.name = file?.data?.meta?.name;
|
||||
}
|
||||
folderItem.root = file.data;
|
||||
if (file?.data?.meta?.seq) {
|
||||
folderItem.seq = file.data?.meta?.seq;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1795,9 +1798,10 @@ export const collectionsSlice = createSlice({
|
||||
currentPath = path.join(currentPath, directoryName);
|
||||
if (!childItem) {
|
||||
childItem = {
|
||||
uid: uuid(),
|
||||
uid: dir?.meta?.uid || uuid(),
|
||||
pathname: currentPath,
|
||||
name: dir?.meta?.name || directoryName,
|
||||
seq: dir?.meta?.seq || 1,
|
||||
filename: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
@@ -1829,6 +1833,9 @@ export const collectionsSlice = createSlice({
|
||||
if (file?.data?.meta?.name) {
|
||||
folderItem.name = file?.data?.meta?.name;
|
||||
}
|
||||
if (file?.data?.meta?.seq) {
|
||||
folderItem.seq = file?.data?.meta?.seq;
|
||||
}
|
||||
folderItem.root = file.data;
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { stringifyIfNot, uuid } from 'utils/common/index';
|
||||
import { uuid } from 'utils/common/index';
|
||||
import { environmentSchema } from '@usebruno/schema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
@@ -90,6 +90,7 @@ export const {
|
||||
export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uid = uuid();
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { name, uid, variables })
|
||||
.then(() => dispatch(_addGlobalEnvironment({ name, uid, variables })))
|
||||
@@ -104,6 +105,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (
|
||||
const globalEnvironments = state.globalEnvironments.globalEnvironments;
|
||||
const baseEnv = globalEnvironments?.find(env => env?.uid == baseEnvUid)
|
||||
const uid = uuid();
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables })
|
||||
.then(() => dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables })))
|
||||
@@ -114,6 +116,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (
|
||||
|
||||
export const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const state = getState();
|
||||
const globalEnvironments = state.globalEnvironments.globalEnvironments;
|
||||
const environment = globalEnvironments?.find(env => env?.uid == environmentUid)
|
||||
@@ -139,6 +142,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-global-environment', {
|
||||
@@ -155,6 +159,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc
|
||||
|
||||
export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:select-global-environment', { environmentUid })
|
||||
.then(() => dispatch(_selectGlobalEnvironment({ environmentUid })))
|
||||
@@ -165,6 +170,7 @@ export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta
|
||||
|
||||
export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-global-environment', { environmentUid })
|
||||
.then(() => dispatch(_deleteGlobalEnvironment({ environmentUid })))
|
||||
@@ -175,6 +181,7 @@ export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta
|
||||
|
||||
export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!globalEnvironmentVariables) resolve();
|
||||
|
||||
const state = getState();
|
||||
|
||||
9
packages/bruno-app/src/selectors/tab.js
Normal file
9
packages/bruno-app/src/selectors/tab.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
export const isTabForItemActive = ({ itemUid }) => createSelector([
|
||||
(state) => state.tabs?.activeTabUid
|
||||
], (activeTabUid) => activeTabUid === itemUid);
|
||||
|
||||
export const isTabForItemPresent = ({ itemUid }) => createSelector([
|
||||
(state) => state.tabs.tabs,
|
||||
], (tabs) => tabs.some((tab) => tab.uid === itemUid));
|
||||
@@ -281,6 +281,12 @@ const darkTheme = {
|
||||
color: 'rgb(52 51 49)'
|
||||
},
|
||||
|
||||
dragAndDrop: {
|
||||
border: '#666666',
|
||||
borderStyle: '2px solid',
|
||||
hoverBg: 'rgba(102, 102, 102, 0.08)',
|
||||
transition: 'all 0.1s ease'
|
||||
},
|
||||
infoTip: {
|
||||
bg: '#1f1f1f',
|
||||
border: '#333333',
|
||||
|
||||
@@ -282,6 +282,12 @@ const lightTheme = {
|
||||
color: 'rgb(152 151 149)'
|
||||
},
|
||||
|
||||
dragAndDrop: {
|
||||
border: '#8b8b8b', // Using the same gray as focusBorder from input
|
||||
borderStyle: '2px solid',
|
||||
hoverBg: 'rgba(139, 139, 139, 0.05)', // Matching the border color with reduced opacity
|
||||
transition: 'all 0.1s ease'
|
||||
},
|
||||
infoTip: {
|
||||
bg: 'white',
|
||||
border: '#e0e0e0',
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
*/
|
||||
|
||||
import { JSHINT } from 'jshint';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
|
||||
@@ -98,6 +98,14 @@ export const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
return findItemByPathname(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
export const findParentItemInCollectionByPathname = (collection, pathname) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return find(flattenedItems, (item) => {
|
||||
return item.items && find(item.items, (i) => i.pathname === pathname);
|
||||
});
|
||||
};
|
||||
|
||||
export const findItemInCollection = (collection, itemUid) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
@@ -150,90 +158,6 @@ export const getItemsLoadStats = (folder) => {
|
||||
};
|
||||
}
|
||||
|
||||
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
if (targetItem.type === 'folder') {
|
||||
targetItem.items = sortBy(targetItem.items || [], (item) => item.seq);
|
||||
targetItem.items.push(draggedItem);
|
||||
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
|
||||
} else {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
// If the dragged item is already at the root of the collection, do nothing
|
||||
if (!draggedItemParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items.push(draggedItem);
|
||||
if (draggedItem.type == 'folder') {
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
|
||||
} else {
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
}
|
||||
};
|
||||
|
||||
export const getItemsToResequence = (parent, collection) => {
|
||||
let itemsToResequence = [];
|
||||
|
||||
if (!parent) {
|
||||
let index = 1;
|
||||
each(collection.items, (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
itemsToResequence.push({
|
||||
pathname: item.pathname,
|
||||
seq: index++
|
||||
});
|
||||
}
|
||||
});
|
||||
return itemsToResequence;
|
||||
}
|
||||
|
||||
if (parent.items && parent.items.length) {
|
||||
let index = 1;
|
||||
each(parent.items, (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
itemsToResequence.push({
|
||||
pathname: item.pathname,
|
||||
seq: index++
|
||||
});
|
||||
}
|
||||
});
|
||||
return itemsToResequence;
|
||||
}
|
||||
|
||||
return itemsToResequence;
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
return map(headers, (header) => {
|
||||
@@ -502,6 +426,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
if (meta?.name) {
|
||||
di.root.meta = {};
|
||||
di.root.meta.name = meta?.name;
|
||||
di.root.meta.seq = meta?.seq;
|
||||
}
|
||||
if (!Object.keys(di.root.request)?.length) {
|
||||
delete di.root.request;
|
||||
@@ -1086,3 +1011,77 @@ export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = []
|
||||
});
|
||||
return credentialsVariables;
|
||||
};
|
||||
|
||||
|
||||
// item sequence utils - START
|
||||
|
||||
export const resetSequencesInFolder = (folderItems) => {
|
||||
const items = folderItems;
|
||||
const sortedItems = items.sort((a, b) => a.seq - b.seq);
|
||||
return sortedItems.map((item, index) => {
|
||||
item.seq = index + 1;
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
export const isItemBetweenSequences = (itemSequence, sourceItemSequence, targetItemSequence) => {
|
||||
if (targetItemSequence > sourceItemSequence) {
|
||||
return itemSequence > sourceItemSequence && itemSequence < targetItemSequence;
|
||||
}
|
||||
return itemSequence < sourceItemSequence && itemSequence >= targetItemSequence;
|
||||
};
|
||||
|
||||
export const calculateNewSequence = (isDraggedItem, targetSequence, draggedSequence) => {
|
||||
if (!isDraggedItem) {
|
||||
return null;
|
||||
}
|
||||
return targetSequence > draggedSequence ? targetSequence - 1 : targetSequence;
|
||||
};
|
||||
|
||||
export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, draggedItemUid }) => {
|
||||
const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));
|
||||
const targetItem = findItem(itemsWithFixedSequences, targetItemUid);
|
||||
const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid);
|
||||
const targetSequence = targetItem?.seq;
|
||||
const draggedSequence = draggedItem?.seq;
|
||||
itemsWithFixedSequences?.forEach(item => {
|
||||
const isDraggedItem = item?.uid === draggedItemUid;
|
||||
const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
if (isBetween) {
|
||||
item.seq += targetSequence > draggedSequence ? -1 : 1;
|
||||
}
|
||||
const newSequence = calculateNewSequence(isDraggedItem, targetSequence, draggedSequence);
|
||||
if (newSequence !== null) {
|
||||
item.seq = newSequence;
|
||||
}
|
||||
});
|
||||
// only return items that have been reordered
|
||||
return itemsWithFixedSequences.filter(item =>
|
||||
items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
);
|
||||
};
|
||||
|
||||
export const getReorderedItemsInSourceDirectory = ({ items }) => {
|
||||
const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));
|
||||
return itemsWithFixedSequences.filter(item =>
|
||||
items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => {
|
||||
const { pathname: targetItemPathname } = targetItem;
|
||||
const { filename: draggedItemFilename } = draggedItem;
|
||||
const targetItemDirname = path.dirname(targetItemPathname);
|
||||
const isTargetTheCollection = targetItemPathname === collectionPathname;
|
||||
const isTargetItemAFolder = isItemAFolder(targetItem);
|
||||
|
||||
if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {
|
||||
return path.join(targetItemPathname, draggedItemFilename)
|
||||
} else if (dropType === 'adjacent') {
|
||||
return path.join(targetItemDirname, draggedItemFilename)
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// item sequence utils - END
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
class Cache {
|
||||
get(key) {
|
||||
return window.localStorage.getItem(key);
|
||||
}
|
||||
set(key, val) {
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Cache();
|
||||
@@ -53,7 +53,7 @@ export const safeStringifyJSON = (obj, indent = false) => {
|
||||
|
||||
export const convertToCodeMirrorJson = (obj) => {
|
||||
try {
|
||||
return JSON5.stringify(obj).slice(1, -1);
|
||||
return JSON.stringify(obj, null, 2).slice(1, -1);
|
||||
} catch (e) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid characters with hyphens
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot or space at end, hyphen allowed
|
||||
const firstCharacter = /^[^\s\-<>:"/\\|?*\x00-\x1F]/; // no space, hyphen and `invalidCharacters`
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no `invalidCharacters`
|
||||
const lastCharacter = /[^.\s<>:"/\\|?*\x00-\x1F]$/; // no dot, space and `invalidCharacters`
|
||||
|
||||
export const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
export const sanitizeName = (name) => {
|
||||
name = name
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[\s\-]+/, '') // remove leading spaces and hyphens
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces
|
||||
return name;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ describe('regex validators', () => {
|
||||
});
|
||||
|
||||
it('should remove trailing periods', () => {
|
||||
expect(sanitizeName('.file')).toBe('file');
|
||||
expect(sanitizeName('.file.')).toBe('file');
|
||||
expect(sanitizeName('.file')).toBe('.file');
|
||||
expect(sanitizeName('.file.')).toBe('.file');
|
||||
expect(sanitizeName('file.')).toBe('file');
|
||||
expect(sanitizeName('file.name.')).toBe('file.name');
|
||||
expect(sanitizeName('hello world.')).toBe('hello world');
|
||||
@@ -83,11 +83,11 @@ describe('regex validators', () => {
|
||||
|
||||
it('should handle filenames with multiple consecutive periods (only remove trailing)', () => {
|
||||
expect(sanitizeName('file.name...')).toBe('file.name');
|
||||
expect(sanitizeName('...file')).toBe('file');
|
||||
expect(sanitizeName('...file')).toBe('...file');
|
||||
expect(sanitizeName('file.name... ')).toBe('file.name');
|
||||
expect(sanitizeName(' ...file')).toBe('file');
|
||||
expect(sanitizeName(' ...file ')).toBe('file');
|
||||
expect(sanitizeName(' ...file.... ')).toBe('file');
|
||||
expect(sanitizeName(' ...file')).toBe('...file');
|
||||
expect(sanitizeName(' ...file ')).toBe('...file');
|
||||
expect(sanitizeName(' ...file.... ')).toBe('...file');
|
||||
});
|
||||
|
||||
it('should handle very long filenames', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { postmanToBruno } from '@usebruno/converters';
|
||||
import { safeParseJSON } from 'utils/common/index';
|
||||
|
||||
const readFile = (files) => {
|
||||
@@ -12,18 +11,15 @@ const readFile = (files) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const importCollection = () => {
|
||||
const postmanToBruno = (collection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
})
|
||||
window.ipcRenderer.invoke('renderer:convert-postman-to-bruno', collection)
|
||||
.then(result => resolve(result))
|
||||
.catch(err => {
|
||||
console.error('Error converting Postman to Bruno via Electron:', err);
|
||||
reject(new BrunoError('Conversion failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
export { postmanToBruno, readFile };
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { resetSequencesInFolder, isItemBetweenSequences } from 'utils/collections/index';
|
||||
|
||||
describe('resetSequencesInFolder', () => {
|
||||
it('should fix the sequences in the folder 1', () => {
|
||||
const folder = {
|
||||
items: [
|
||||
{ uid: '1', seq: 1 },
|
||||
{ uid: '2', seq: 3 },
|
||||
{ uid: '3', seq: 6 },
|
||||
],
|
||||
};
|
||||
|
||||
const fixedFolder = resetSequencesInFolder(folder.items);
|
||||
expect(fixedFolder).toEqual([
|
||||
{ uid: '1', seq: 1 },
|
||||
{ uid: '2', seq: 2 },
|
||||
{ uid: '3', seq: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should fix the sequences in the folder 2', () => {
|
||||
const folder = {
|
||||
items: [
|
||||
{ uid: '1', seq: 3 },
|
||||
{ uid: '2', seq: 1 },
|
||||
{ uid: '3', seq: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const fixedFolder = resetSequencesInFolder(folder.items);
|
||||
expect(fixedFolder).toEqual([
|
||||
{ uid: '2', seq: 1 },
|
||||
{ uid: '3', seq: 2 },
|
||||
{ uid: '1', seq: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fix the sequences in the folder with missing sequences', () => {
|
||||
const folder = {
|
||||
items: [
|
||||
{ uid: '1', seq: 1 },
|
||||
{ uid: '2', type: 'folder' },
|
||||
{ uid: '3', type: 'folder' },
|
||||
{ uid: '4', seq: 7 },
|
||||
]
|
||||
};
|
||||
|
||||
const fixedFolder = resetSequencesInFolder(folder.items);
|
||||
expect(fixedFolder).toEqual([
|
||||
{ uid: '1', seq: 1 },
|
||||
{ uid: '2', seq: 2, type: 'folder' },
|
||||
{ uid: '3', seq: 3, type: 'folder' },
|
||||
{ uid: '4', seq: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fix the sequences in the folder with same sequences', () => {
|
||||
const folder = {
|
||||
items: [
|
||||
{ uid: '1', seq: 2 },
|
||||
{ uid: '2', seq: 2 },
|
||||
{ uid: '3', seq: 3 },
|
||||
{ uid: '4', seq: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const fixedFolder = resetSequencesInFolder(folder.items);
|
||||
expect(fixedFolder).toEqual([
|
||||
{ uid: '4', seq: 1 },
|
||||
{ uid: '1', seq: 2 },
|
||||
{ uid: '2', seq: 3 },
|
||||
{ uid: '3', seq: 4 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isItemBetweenSequences', () => {
|
||||
it('should return true if the item is between the sequences 1', () => {
|
||||
const item = { uid: '1', seq: 2 };
|
||||
const draggedSequence = 1;
|
||||
const targetSequence = 5;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the item is between the sequences 2', () => {
|
||||
const item = { uid: '1', seq: 2 };
|
||||
const draggedSequence = 1;
|
||||
const targetSequence = 5;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the item is between the sequences 3', () => {
|
||||
const item = { uid: '1', seq: 4 };
|
||||
const draggedSequence = 1;
|
||||
const targetSequence = 5;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the item is between the sequences 4', () => {
|
||||
const item = { uid: '1', seq: 1 };
|
||||
const draggedSequence = 5;
|
||||
const targetSequence = 1;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the item is between the sequences 1', () => {
|
||||
const item = { uid: '1', seq: 1 };
|
||||
const draggedSequence = 1;
|
||||
const targetSequence = 5;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the item is between the sequences 2', () => {
|
||||
const item = { uid: '1', seq: 5 };
|
||||
const draggedSequence = 1;
|
||||
const targetSequence = 5;
|
||||
const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,7 @@
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
@@ -63,6 +64,7 @@
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
|
||||
@@ -58,6 +58,44 @@ If you need to limit the trusted CA to a specified set when validating the reque
|
||||
bru run request.bru --cacert myCustomCA.pem --ignore-truststore
|
||||
```
|
||||
|
||||
## Importing Collections
|
||||
|
||||
You can import collections from other formats, such as OpenAPI, using the import command:
|
||||
|
||||
```bash
|
||||
bru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"
|
||||
```
|
||||
|
||||
You can also use the shorter form with aliases:
|
||||
|
||||
```bash
|
||||
bru import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"
|
||||
```
|
||||
|
||||
This creates a Bruno collection directory that can be opened in Bruno.
|
||||
|
||||
You can also import directly from a URL:
|
||||
|
||||
```bash
|
||||
bru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"
|
||||
```
|
||||
|
||||
You can also export the collection as a JSON file:
|
||||
|
||||
```bash
|
||||
bru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"
|
||||
```
|
||||
|
||||
Import Options:
|
||||
|
||||
| Option | Details |
|
||||
| ------------------------- | -------------------------------------------------- |
|
||||
| --source, -s | Path to the source file or URL (required) |
|
||||
| --output, -o | Path to the output directory |
|
||||
| --output-file, -f | Path to the output JSON file |
|
||||
| --collection-name, -n | Name for the imported collection |
|
||||
| --insecure | Skip SSL certificate validation when fetching from URLs |
|
||||
|
||||
## Command Line Options
|
||||
|
||||
| Option | Details |
|
||||
|
||||
230
packages/bruno-cli/src/commands/import.js
Normal file
230
packages/bruno-cli/src/commands/import.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const jsyaml = require('js-yaml');
|
||||
const axios = require('axios');
|
||||
const { openApiToBruno } = require('@usebruno/converters');
|
||||
const { exists, isDirectory, sanitizeName } = require('../utils/filesystem');
|
||||
const { createCollectionFromBrunoObject } = require('../utils/collection');
|
||||
|
||||
const command = 'import <type>';
|
||||
const desc = 'Import a collection from other formats';
|
||||
|
||||
const builder = (yargs) => {
|
||||
yargs
|
||||
.positional('type', {
|
||||
describe: 'Type of collection to import',
|
||||
type: 'string',
|
||||
choices: ['openapi']
|
||||
})
|
||||
.option('source', {
|
||||
alias: 's',
|
||||
describe: 'Path to the source file or URL',
|
||||
type: 'string',
|
||||
demandOption: true
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
describe: 'Path to the output directory',
|
||||
type: 'string',
|
||||
conflicts: 'output-file'
|
||||
})
|
||||
.option('output-file', {
|
||||
alias: 'f',
|
||||
describe: 'Path to the output JSON file',
|
||||
type: 'string',
|
||||
conflicts: 'output'
|
||||
})
|
||||
.option('collection-name', {
|
||||
alias: 'n',
|
||||
describe: 'Name for the imported collection',
|
||||
type: 'string'
|
||||
})
|
||||
.option('insecure', {
|
||||
type: 'boolean',
|
||||
describe: 'Skip SSL certificate verification when fetching from URLs',
|
||||
default: false
|
||||
})
|
||||
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"')
|
||||
.example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"')
|
||||
.example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"')
|
||||
.example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop')
|
||||
.example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"')
|
||||
.example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"');
|
||||
};
|
||||
|
||||
const isUrl = (str) => {
|
||||
try {
|
||||
return Boolean(new URL(str));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readOpenApiFile = async (source, options = {}) => {
|
||||
try {
|
||||
let content;
|
||||
|
||||
if (isUrl(source)) {
|
||||
// Handle URL input
|
||||
console.log(chalk.yellow(`Fetching specification from URL: ${source}`));
|
||||
try {
|
||||
const axiosOptions = {
|
||||
timeout: 30000, // 30 second timeout
|
||||
maxContentLength: 10 * 1024 * 1024,
|
||||
validateStatus: status => status >= 200 && status < 300
|
||||
};
|
||||
|
||||
// Skip SSL certificate validation if insecure flag is set
|
||||
if (options.insecure) {
|
||||
console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));
|
||||
axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
const response = await axios.get(source, axiosOptions);
|
||||
content = response.data;
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
throw new Error('Request timed out. The server took too long to respond.');
|
||||
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
|
||||
error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
||||
throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);
|
||||
} else if (error.response) {
|
||||
throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);
|
||||
} else if (error.request) {
|
||||
throw new Error(`No response received from server. Check the URL and your network connection.`);
|
||||
} else {
|
||||
throw new Error(`Error fetching URL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If response is already an object, return it directly
|
||||
if (typeof content === 'object' && content !== null) {
|
||||
return content;
|
||||
}
|
||||
} else {
|
||||
// Handle file input
|
||||
if (!await exists(source)) {
|
||||
throw new Error(`File does not exist: ${source}`);
|
||||
}
|
||||
content = fs.readFileSync(source, 'utf8');
|
||||
}
|
||||
|
||||
// If content is a string, try to parse as JSON or YAML
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (jsonError) {
|
||||
try {
|
||||
return jsyaml.load(content);
|
||||
} catch (yamlError) {
|
||||
throw new Error('Failed to parse content as JSON or YAML');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
// Let the specific error handling from above propagate
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handler = async (argv) => {
|
||||
try {
|
||||
const { type, source, output, outputFile, collectionName, insecure } = argv;
|
||||
|
||||
if (!type || type !== 'openapi') {
|
||||
console.error(chalk.red('Only OpenAPI import is supported currently'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
console.error(chalk.red('Source file or URL is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!output && !outputFile) {
|
||||
console.error(chalk.red('Either --output or --output-file is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));
|
||||
|
||||
const openApiSpec = await readOpenApiFile(source, { insecure });
|
||||
|
||||
if (!openApiSpec) {
|
||||
console.error(chalk.red('Failed to parse OpenAPI specification'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
|
||||
|
||||
// Convert OpenAPI to Bruno format
|
||||
let brunoCollection = openApiToBruno(openApiSpec);
|
||||
|
||||
// Override collection name if provided
|
||||
if (collectionName) {
|
||||
brunoCollection.name = collectionName;
|
||||
}
|
||||
|
||||
if (outputFile) {
|
||||
// Save as JSON file
|
||||
const outputPath = path.resolve(outputFile);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2));
|
||||
console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`));
|
||||
} else if (output) {
|
||||
const resolvedOutput = path.resolve(output);
|
||||
|
||||
// Check if output is an existing directory
|
||||
const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput);
|
||||
|
||||
// Determine the final output directory
|
||||
let outputDir;
|
||||
if (isOutputDirectory) {
|
||||
// If output is an existing directory, use collection name to create a subdirectory
|
||||
const dirName = sanitizeName(brunoCollection.name);
|
||||
outputDir = path.join(resolvedOutput, dirName);
|
||||
|
||||
// Check if this subfolder already exists
|
||||
if (await exists(outputDir)) {
|
||||
const dirContents = fs.readdirSync(outputDir);
|
||||
if (dirContents.length > 0) {
|
||||
console.error(chalk.red(`Output directory is not empty: ${outputDir}`));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Create the subfolder
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
// If output doesn't exist or is not a directory, use it directly
|
||||
outputDir = resolvedOutput;
|
||||
|
||||
// Check if parent directory exists
|
||||
const parentDir = path.dirname(outputDir);
|
||||
if (!await exists(parentDir)) {
|
||||
console.error(chalk.red(`Parent directory does not exist: ${parentDir}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
await createCollectionFromBrunoObject(brunoCollection, outputDir);
|
||||
console.log(chalk.green(`Bruno collection created at ${outputDir}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
command,
|
||||
desc,
|
||||
builder,
|
||||
handler,
|
||||
isUrl,
|
||||
readOpenApiFile
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang');
|
||||
const { sanitizeName } = require('./filesystem');
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
@@ -219,6 +223,136 @@ const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
|
||||
|
||||
const getAllRequestsAtFolderRoot = (folderItems = []) => {
|
||||
return getAllRequestsInFolder(folderItems, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe write file implementation to handle errors
|
||||
* @param {string} filePath - Path to write file
|
||||
* @param {string} content - Content to write
|
||||
*/
|
||||
const safeWriteFileSync = (filePath, content) => {
|
||||
try {
|
||||
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
console.error(`Error writing file ${filePath}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Bruno collection directory structure from a Bruno collection object
|
||||
*
|
||||
* @param {Object} collection - The Bruno collection object
|
||||
* @param {string} dirPath - The output directory path
|
||||
*/
|
||||
const createCollectionFromBrunoObject = async (collection, dirPath) => {
|
||||
// Create bruno.json
|
||||
const brunoConfig = {
|
||||
version: '1',
|
||||
name: collection.name,
|
||||
type: 'collection',
|
||||
ignore: ['node_modules', '.git']
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dirPath, 'bruno.json'),
|
||||
JSON.stringify(brunoConfig, null, 2)
|
||||
);
|
||||
|
||||
// Create collection.bru if root exists
|
||||
if (collection.root) {
|
||||
const collectionContent = await jsonToCollectionBru(collection.root);
|
||||
fs.writeFileSync(path.join(dirPath, 'collection.bru'), collectionContent);
|
||||
}
|
||||
|
||||
// Process environments
|
||||
if (collection.environments && collection.environments.length) {
|
||||
const envDirPath = path.join(dirPath, 'environments');
|
||||
fs.mkdirSync(envDirPath, { recursive: true });
|
||||
|
||||
for (const env of collection.environments) {
|
||||
const content = await envJsonToBruV2(env);
|
||||
const filename = sanitizeName(`${env.name}.bru`);
|
||||
fs.writeFileSync(path.join(envDirPath, filename), content);
|
||||
}
|
||||
}
|
||||
|
||||
// Process collection items
|
||||
await processCollectionItems(collection.items, dirPath);
|
||||
|
||||
return dirPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively processes collection items to create files and folders
|
||||
*
|
||||
* @param {Array} items - Collection items
|
||||
* @param {string} currentPath - Current directory path
|
||||
*/
|
||||
const processCollectionItems = async (items = [], currentPath) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'folder') {
|
||||
// Create folder
|
||||
let sanitizedFolderName = sanitizeName(item?.filename || item?.name);
|
||||
const folderPath = path.join(currentPath, sanitizedFolderName);
|
||||
fs.mkdirSync(folderPath, { recursive: true });
|
||||
|
||||
// Create folder.bru file if root exists
|
||||
if (item?.root?.meta?.name) {
|
||||
const folderBruFilePath = path.join(folderPath, 'folder.bru');
|
||||
if (item.seq) {
|
||||
item.root.meta.seq = item.seq;
|
||||
}
|
||||
const folderContent = await jsonToCollectionBru(
|
||||
item.root,
|
||||
true
|
||||
);
|
||||
safeWriteFileSync(folderBruFilePath, folderContent);
|
||||
}
|
||||
|
||||
// Process folder items recursively
|
||||
if (item.items && item.items.length) {
|
||||
await processCollectionItems(item.items, folderPath);
|
||||
}
|
||||
} else if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
// Create request file
|
||||
let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
|
||||
if (!sanitizedFilename.endsWith('.bru')) {
|
||||
sanitizedFilename += '.bru';
|
||||
}
|
||||
|
||||
// Convert JSON to BRU format based on the item type
|
||||
let type = item.type === 'http-request' ? 'http' : 'graphql';
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: item.name,
|
||||
type: type,
|
||||
seq: typeof item.seq === 'number' ? item.seq : 1
|
||||
},
|
||||
http: {
|
||||
method: (item.request?.method || 'GET').toLowerCase(),
|
||||
url: item.request?.url || '',
|
||||
auth: item.request?.auth?.mode || 'none',
|
||||
body: item.request?.body?.mode || 'none'
|
||||
},
|
||||
params: item.request?.params || [],
|
||||
headers: item.request?.headers || [],
|
||||
auth: item.request?.auth || {},
|
||||
body: item.request?.body || {},
|
||||
script: item.request?.script || {},
|
||||
vars: {
|
||||
req: item.request?.vars?.req || [],
|
||||
res: item.request?.vars?.res || []
|
||||
},
|
||||
assertions: item.request?.assertions || [],
|
||||
tests: item.request?.tests || '',
|
||||
docs: item.request?.docs || ''
|
||||
};
|
||||
|
||||
// Convert to BRU format and write to file
|
||||
const content = await jsonToBruV2(bruJson);
|
||||
safeWriteFileSync(path.join(currentPath, sanitizedFilename), content);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -228,5 +362,6 @@ module.exports = {
|
||||
findItemInCollection,
|
||||
getTreePathFromCollectionToItem,
|
||||
getAllRequestsInFolder,
|
||||
getAllRequestsAtFolderRoot
|
||||
getAllRequestsAtFolderRoot,
|
||||
createCollectionFromBrunoObject
|
||||
}
|
||||
@@ -118,6 +118,46 @@ const getSubDirectories = (dir) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitizes a filename to make it safe for filesystem operations
|
||||
*
|
||||
* @param {string} name - The name to sanitize
|
||||
* @returns {string} - The sanitized name
|
||||
*/
|
||||
const sanitizeName = (name) => {
|
||||
if (!name) return '';
|
||||
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g;
|
||||
return name
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates if a name is valid for the filesystem
|
||||
*
|
||||
* @param {string} name - The name to validate
|
||||
* @returns {boolean} - True if the name is valid, false otherwise
|
||||
*/
|
||||
const validateName = (name) => {
|
||||
if (!name) return false;
|
||||
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
|
||||
|
||||
if (name.length > 255) return false; // max name length
|
||||
if (reservedDeviceNames.test(name)) return false; // windows reserved names
|
||||
|
||||
return (
|
||||
firstCharacter.test(name) &&
|
||||
middleCharacters.test(name) &&
|
||||
lastCharacter.test(name)
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
exists,
|
||||
isSymbolicLink,
|
||||
@@ -131,5 +171,7 @@ module.exports = {
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
stripExtension,
|
||||
getSubDirectories
|
||||
getSubDirectories,
|
||||
sanitizeName,
|
||||
validateName
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { T_RunnerResults } from "../../types";
|
||||
import { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from "../../utils";
|
||||
import { getRunnerSummary } from "../../runner-summary";
|
||||
import htmlTemplateString from "./template";
|
||||
|
||||
const generateHtmlReport = ({
|
||||
@@ -8,7 +7,7 @@ const generateHtmlReport = ({
|
||||
}: {
|
||||
runnerResults: T_RunnerResults[]
|
||||
}): string => {
|
||||
const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results }) => {
|
||||
const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results, summary }) => {
|
||||
return {
|
||||
iterationIndex,
|
||||
results: results.map((result) => {
|
||||
@@ -29,7 +28,7 @@ const generateHtmlReport = ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
summary: getRunnerSummary(results)
|
||||
summary
|
||||
}
|
||||
});
|
||||
const htmlString = htmlTemplateString(encodeBase64(JSON.stringify(resultsWithSummaryAndCleanData)));
|
||||
|
||||
@@ -96,6 +96,7 @@ export type T_RunnerResults = {
|
||||
iterationIndex: number;
|
||||
iterationData?: any; // todo - csv/json row data
|
||||
results: T_RunnerRequestExecutionResult[];
|
||||
summary: T_RunSummary;
|
||||
}
|
||||
|
||||
// run summary type
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"dependencies": {
|
||||
"@usebruno/schema": "^0.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jscodeshift": "^17.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "3.3.8"
|
||||
},
|
||||
@@ -31,6 +32,7 @@
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@web/rollup-plugin-copy": "^0.5.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
|
||||
@@ -2,6 +2,7 @@ const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
const { copy } = require('@web/rollup-plugin-copy');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const alias = require('@rollup/plugin-alias');
|
||||
@@ -12,12 +13,12 @@ module.exports = [
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
dir: path.dirname(packageJson.main),
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
dir: path.dirname(packageJson.module),
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
@@ -32,6 +33,10 @@ module.exports = [
|
||||
terser(),
|
||||
alias({
|
||||
entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }]
|
||||
}),
|
||||
copy({
|
||||
patterns: 'src/workers/scripts/**/*',
|
||||
rootDir: '.'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const validateSchema = (collection = {}) => {
|
||||
collectionSchema.validateSync(collection);
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.log("Error validating schema", err);
|
||||
throw new Error('The Collection has an invalid schema');
|
||||
}
|
||||
};
|
||||
|
||||
3
packages/bruno-converters/src/constants/index.js
Normal file
3
packages/bruno-converters/src/constants/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { invalidVariableCharacterRegex } from './regex';
|
||||
|
||||
export { invalidVariableCharacterRegex };
|
||||
1
packages/bruno-converters/src/constants/regex.js
Normal file
1
packages/bruno-converters/src/constants/regex.js
Normal file
@@ -0,0 +1 @@
|
||||
export const invalidVariableCharacterRegex = /[^\w-.]/g;
|
||||
@@ -2,4 +2,5 @@ export { default as postmanToBruno } from './postman/postman-to-bruno.js';
|
||||
export { default as postmanToBrunoEnvironment } from './postman/postman-env-to-bruno-env.js';
|
||||
export { default as brunoToPostman } from './postman/bruno-to-postman.js';
|
||||
export { default as openApiToBruno } from './openapi/openapi-to-bruno.js';
|
||||
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
|
||||
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
|
||||
export { default as postmanTranslation } from './postman/postman-translations.js';
|
||||
@@ -1,4 +1,7 @@
|
||||
import each from 'lodash/each';
|
||||
import { invalidVariableCharacterRegex } from '../constants';
|
||||
import { uuid } from '../common';
|
||||
|
||||
const isSecret = (type) => {
|
||||
return type === 'secret';
|
||||
};
|
||||
@@ -8,7 +11,8 @@ const importPostmanEnvironmentVariables = (brunoEnvironment, values) => {
|
||||
|
||||
each(values, (i) => {
|
||||
const brunoEnvironmentVariable = {
|
||||
name: i.key,
|
||||
uid: uuid(),
|
||||
name: i.key.replace(invalidVariableCharacterRegex, '_'),
|
||||
value: i.value,
|
||||
enabled: i.enabled,
|
||||
secret: isSecret(i.type)
|
||||
|
||||
@@ -2,6 +2,7 @@ import get from 'lodash/get';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
import each from 'lodash/each';
|
||||
import postmanTranslation from './postman-translations';
|
||||
import { invalidVariableCharacterRegex } from '../constants/index';
|
||||
|
||||
const parseGraphQLRequest = (graphqlSource) => {
|
||||
try {
|
||||
@@ -93,17 +94,10 @@ const importScriptsFromEvents = (events, requestObject) => {
|
||||
requestObject.script = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
requestObject.script.req = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
requestObject.script.req = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.script.req = postmanTranslation(event.script.exec);
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.script.req = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
requestObject.script.req = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
@@ -113,17 +107,10 @@ const importScriptsFromEvents = (events, requestObject) => {
|
||||
requestObject.tests = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
requestObject.tests = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.tests = postmanTranslation(event.script.exec);
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.tests = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
@@ -134,7 +121,7 @@ const importScriptsFromEvents = (events, requestObject) => {
|
||||
const importCollectionLevelVariables = (variables, requestObject) => {
|
||||
const vars = variables.map((v) => ({
|
||||
uid: uuid(),
|
||||
name: v.key,
|
||||
name: v.key.replace(invalidVariableCharacterRegex, '_'),
|
||||
value: v.value,
|
||||
enabled: true
|
||||
}));
|
||||
@@ -246,13 +233,13 @@ const processAuth = (auth, requestObject) => {
|
||||
}
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorkers = false } = {}, scriptMap)=> {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
const requestMap = {};
|
||||
const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE']
|
||||
|
||||
each(item, (i) => {
|
||||
item.forEach((i, index) => {
|
||||
if (isItemAFolder(i)) {
|
||||
const baseFolderName = i.name || 'Untitled Folder';
|
||||
let folderName = baseFolderName;
|
||||
@@ -268,6 +255,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
name: folderName,
|
||||
type: 'folder',
|
||||
items: [],
|
||||
seq: index + 1,
|
||||
root: {
|
||||
docs: i.description || '',
|
||||
meta: {
|
||||
@@ -291,6 +279,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
}
|
||||
};
|
||||
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
|
||||
// Folder level auth
|
||||
if (i.auth) {
|
||||
processAuth(i.auth, brunoFolderItem.root.request);
|
||||
@@ -300,221 +290,221 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
}
|
||||
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, { useWorkers }, scriptMap);
|
||||
}
|
||||
|
||||
if (i.event) {
|
||||
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
|
||||
if(useWorkers) {
|
||||
scriptMap.set(brunoFolderItem.uid, {
|
||||
events: i.event,
|
||||
request: brunoFolderItem.root.request
|
||||
});
|
||||
} else {
|
||||
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
|
||||
}
|
||||
}
|
||||
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
|
||||
} else {
|
||||
if (i.request) {
|
||||
if (!requestMethods.includes(i?.request?.method.toUpperCase())) {
|
||||
console.warn('Unexpected request.method', i?.request?.method);
|
||||
return;
|
||||
} else if (i.request) {
|
||||
if (!requestMethods.includes(i?.request?.method.toUpperCase())) {
|
||||
console.warn('Unexpected request.method', i?.request?.method);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseRequestName = i.name || 'Untitled Request';
|
||||
let requestName = baseRequestName;
|
||||
let count = 1;
|
||||
|
||||
while (requestMap[requestName]) {
|
||||
requestName = `${baseRequestName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const url = constructUrl(i.request.url);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: requestName,
|
||||
type: 'http-request',
|
||||
seq: index + 1,
|
||||
request: {
|
||||
url: url,
|
||||
method: i?.request?.method?.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
docs: i.request.description || ''
|
||||
}
|
||||
};
|
||||
|
||||
const baseRequestName = i.name || 'Untitled Request';
|
||||
let requestName = baseRequestName;
|
||||
let count = 1;
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
|
||||
while (requestMap[requestName]) {
|
||||
requestName = `${baseRequestName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const url = constructUrl(i.request.url);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: requestName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: url,
|
||||
method: i?.request?.method?.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
docs: i.request.description || ''
|
||||
}
|
||||
};
|
||||
|
||||
if (i.event) {
|
||||
if (i.event) {
|
||||
if(useWorkers) {
|
||||
scriptMap.set(brunoRequestItem.uid, {
|
||||
events: i.event,
|
||||
request: brunoRequestItem.request
|
||||
});
|
||||
} else {
|
||||
i.event.forEach((event) => {
|
||||
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.script) {
|
||||
if (!brunoRequestItem.request?.script) {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.script.req = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.script.req = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
brunoRequestItem.request.script.req = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.tests) {
|
||||
if (!brunoRequestItem.request?.tests) {
|
||||
brunoRequestItem.request.tests = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.tests = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.tests = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.tests = postmanTranslation(event.script.exec);
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.tests = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
brunoRequestItem.request.tests = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const bodyMode = get(i, 'request.body.mode');
|
||||
if (bodyMode) {
|
||||
if (bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
|
||||
each(i.request.body.formdata, (param) => {
|
||||
const isFile = param.type === 'file';
|
||||
let value;
|
||||
let type;
|
||||
|
||||
if (isFile) {
|
||||
// If param.src is an array, keep it as it is.
|
||||
// If param.src is a string, convert it into an array with a single element.
|
||||
value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null;
|
||||
type = 'file';
|
||||
} else {
|
||||
value = param.value;
|
||||
type = 'text';
|
||||
}
|
||||
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: type,
|
||||
name: param.key,
|
||||
value: value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(i.request.body.urlencoded, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'raw') {
|
||||
let language = get(i, 'request.body.options.raw.language');
|
||||
if (!language) {
|
||||
language = searchLanguageByHeader(i.request.header);
|
||||
}
|
||||
if (language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
} else if (language === 'xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = i.request.body.raw;
|
||||
} else {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = i.request.body.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMode === 'graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
|
||||
}
|
||||
|
||||
each(i.request.header, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.key,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
// Handle request-level auth or inherit from parent
|
||||
const auth = i.request.auth ?? parentAuth;
|
||||
processAuth(auth, brunoRequestItem.request);
|
||||
|
||||
each(get(i, 'request.url.query'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.variable', []), (param) => {
|
||||
if (!param.key) {
|
||||
// If no key, skip this iteration and discard the param
|
||||
return;
|
||||
}
|
||||
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value ?? '',
|
||||
description: param.description ?? '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
requestMap[requestName] = brunoRequestItem;
|
||||
}
|
||||
|
||||
const bodyMode = get(i, 'request.body.mode');
|
||||
if (bodyMode) {
|
||||
if (bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
|
||||
each(i.request.body.formdata, (param) => {
|
||||
const isFile = param.type === 'file';
|
||||
let value;
|
||||
let type;
|
||||
|
||||
if (isFile) {
|
||||
// If param.src is an array, keep it as it is.
|
||||
// If param.src is a string, convert it into an array with a single element.
|
||||
value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null;
|
||||
type = 'file';
|
||||
} else {
|
||||
value = param.value;
|
||||
type = 'text';
|
||||
}
|
||||
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: type,
|
||||
name: param.key,
|
||||
value: value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(i.request.body.urlencoded, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'raw') {
|
||||
let language = get(i, 'request.body.options.raw.language');
|
||||
if (!language) {
|
||||
language = searchLanguageByHeader(i.request.header);
|
||||
}
|
||||
if (language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
} else if (language === 'xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = i.request.body.raw;
|
||||
} else {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = i.request.body.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMode === 'graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
|
||||
}
|
||||
|
||||
each(i.request.header, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.key,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
// Handle request-level auth or inherit from parent
|
||||
const auth = i.request.auth ?? parentAuth;
|
||||
processAuth(auth, brunoRequestItem.request);
|
||||
|
||||
each(get(i, 'request.url.query'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.variable', []), (param) => {
|
||||
if (!param.key) {
|
||||
// If no key, skip this iteration and discard the param
|
||||
return;
|
||||
}
|
||||
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value ?? '',
|
||||
description: param.description ?? '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
requestMap[requestName] = brunoRequestItem;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const searchLanguageByHeader = (headers) => {
|
||||
let contentType;
|
||||
each(headers, (header) => {
|
||||
@@ -530,7 +520,7 @@ const searchLanguageByHeader = (headers) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const importPostmanV2Collection = async (collection, { useWorkers = false }) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name || 'Untitled Collection',
|
||||
uid: uuid(),
|
||||
@@ -571,12 +561,74 @@ const importPostmanV2Collection = (collection) => {
|
||||
// Collection level auth
|
||||
processAuth(collection.auth, brunoCollection.root.request);
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
|
||||
|
||||
// Create a single scriptMap for all items
|
||||
const scriptMap = useWorkers ? new Map() : null;
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, { useWorkers }, scriptMap);
|
||||
|
||||
// Process all scripts in a single call at the top level
|
||||
if (useWorkers && scriptMap && scriptMap.size > 0) {
|
||||
try {
|
||||
const { default: scriptTranslationWorker } = await import('../workers/postman-translator-worker');
|
||||
const translatedScripts = await scriptTranslationWorker(scriptMap);
|
||||
|
||||
// Apply translated scripts to all items in the collection
|
||||
const applyScriptsToItems = (items) => {
|
||||
items.forEach(item => {
|
||||
if (item.type === 'folder') {
|
||||
// Apply scripts to the folder
|
||||
if (translatedScripts.has(item.uid)) {
|
||||
if (!item.root.request.script) {
|
||||
item.root.request.script = {};
|
||||
}
|
||||
if (!item.root.request.tests) {
|
||||
item.root.request.tests = '';
|
||||
}
|
||||
|
||||
const script = translatedScripts.get(item.uid).request?.script?.req;
|
||||
const tests = translatedScripts.get(item.uid).request?.tests;
|
||||
|
||||
item.root.request.script.req = script && script.length > 0 ? script : '';
|
||||
item.root.request.tests = tests && tests.length > 0 ? tests : '';
|
||||
}
|
||||
|
||||
// Recursively apply to nested items
|
||||
if (item.items && item.items.length > 0) {
|
||||
applyScriptsToItems(item.items);
|
||||
}
|
||||
} else {
|
||||
if (translatedScripts.has(item.uid)) {
|
||||
if (!item.request.script) {
|
||||
item.request.script = {};
|
||||
}
|
||||
if (!item.request.tests) {
|
||||
item.request.tests = '';
|
||||
}
|
||||
|
||||
const script = translatedScripts.get(item.uid).request?.script?.req;
|
||||
const tests = translatedScripts.get(item.uid).request?.tests;
|
||||
|
||||
item.request.script.req = script && script.length > 0 ? script : '';
|
||||
item.request.tests = tests && tests.length > 0 ? tests : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
applyScriptsToItems(brunoCollection.items);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in script translation worker:', error);
|
||||
} finally {
|
||||
scriptMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
const parsePostmanCollection = (collection) => {
|
||||
|
||||
const parsePostmanCollection = async (collection, { useWorkers = false }) => {
|
||||
try {
|
||||
let schema = get(collection, 'info.schema');
|
||||
|
||||
@@ -588,7 +640,7 @@ const parsePostmanCollection = (collection) => {
|
||||
];
|
||||
|
||||
if (v2Schemas.includes(schema)) {
|
||||
return importPostmanV2Collection(collection);
|
||||
return await importPostmanV2Collection(collection, { useWorkers });
|
||||
}
|
||||
|
||||
throw new Error('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.');
|
||||
@@ -602,9 +654,10 @@ const parsePostmanCollection = (collection) => {
|
||||
}
|
||||
};
|
||||
|
||||
const postmanToBruno = (postmanCollection) => {
|
||||
const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => {
|
||||
try {
|
||||
const parsedPostmanCollection = parsePostmanCollection(postmanCollection);
|
||||
|
||||
const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers });
|
||||
const transformedCollection = transformItemsInCollection(parsedPostmanCollection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
const validatedCollection = validateSchema(hydratedCollection);
|
||||
@@ -615,4 +668,5 @@ const postmanToBruno = (postmanCollection) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default postmanToBruno;
|
||||
@@ -1,3 +1,5 @@
|
||||
import translateCode from '../utils/jscode-shift-translator';
|
||||
|
||||
const replacements = {
|
||||
'pm\\.environment\\.get\\(': 'bru.getEnvVar(',
|
||||
'pm\\.environment\\.set\\(': 'bru.setEnvVar(',
|
||||
@@ -20,7 +22,7 @@ const replacements = {
|
||||
'pm\\.response\\.responseTime': 'res.getResponseTime()',
|
||||
'pm\\.environment\\.name': 'bru.getEnvName()',
|
||||
'pm\\.response\\.status': 'res.statusText',
|
||||
'pm\\.response\\.headers': 'req.getHeaders()',
|
||||
'pm\\.response\\.headers': 'res.getHeaders()',
|
||||
"tests\\['([^']+)'\\]\\s*=\\s*([^;]+);": 'test("$1", function() { expect(Boolean($2)).to.be.true; });',
|
||||
'pm\\.request\\.url': 'req.getUrl()',
|
||||
'pm\\.request\\.method': 'req.getMethod()',
|
||||
@@ -48,22 +50,38 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern,
|
||||
replacement
|
||||
}));
|
||||
|
||||
const postmanTranslation = (script) => {
|
||||
const processRegexReplacement = (code) => {
|
||||
for (const { regex, replacement } of compiledReplacements) {
|
||||
if (regex.test(code)) {
|
||||
code = code.replace(regex, replacement);
|
||||
|
||||
}
|
||||
}
|
||||
if ((code.includes('pm.') || code.includes('postman.'))) {
|
||||
code = code.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
|
||||
const postmanTranslation = (script, options = {}) => {
|
||||
let modifiedScript = Array.isArray(script) ? script.join('\n') : script;
|
||||
|
||||
try {
|
||||
let modifiedScript = script;
|
||||
let modified = false;
|
||||
for (const { regex, replacement } of compiledReplacements) {
|
||||
if (regex.test(modifiedScript)) {
|
||||
modifiedScript = modifiedScript.replace(regex, replacement);
|
||||
modified = true;
|
||||
}
|
||||
let translatedCode = translateCode(modifiedScript);
|
||||
if ((translatedCode.includes('pm.') || translatedCode.includes('postman.'))) {
|
||||
translatedCode = translatedCode.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
}
|
||||
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
|
||||
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
}
|
||||
return modifiedScript;
|
||||
return translatedCode;
|
||||
} catch (e) {
|
||||
return script;
|
||||
console.warn('Error in postman translation:', e);
|
||||
|
||||
try {
|
||||
return processRegexReplacement(modifiedScript);
|
||||
} catch (e) {
|
||||
console.warn('Error in postman translation:', e);
|
||||
return modifiedScript;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
680
packages/bruno-converters/src/utils/jscode-shift-translator.js
Normal file
680
packages/bruno-converters/src/utils/jscode-shift-translator.js
Normal file
@@ -0,0 +1,680 @@
|
||||
const j = require('jscodeshift');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
|
||||
/**
|
||||
* Efficiently builds a string representation of a member expression without using toSource()
|
||||
*
|
||||
* @param {Object} node - The member expression node from the AST
|
||||
* @return {string} - String representation of the member expression (e.g., "pm.environment.get")
|
||||
*/
|
||||
function getMemberExpressionString(node) {
|
||||
// Handle base case: if this is an Identifier
|
||||
if (node.type === 'Identifier') {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
// Handle member expressions
|
||||
if (node.type === 'MemberExpression') {
|
||||
const objectStr = getMemberExpressionString(node.object);
|
||||
|
||||
// For computed properties like obj[prop], we need special handling
|
||||
if (node.computed) {
|
||||
// For literals like obj["prop"], we can include them in the string
|
||||
if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
|
||||
return `${objectStr}.${node.property.value}`;
|
||||
}
|
||||
// For other computed properties, we can't reliably represent them as a simple string
|
||||
return `${objectStr}.[computed]`;
|
||||
}
|
||||
|
||||
// For regular property access like obj.prop
|
||||
if (node.property.type === 'Identifier') {
|
||||
return `${objectStr}.${node.property.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '[unsupported]';
|
||||
}
|
||||
|
||||
// Simple 1:1 translations for straightforward replacements
|
||||
const simpleTranslations = {
|
||||
// Environment variables
|
||||
'pm.environment.get': 'bru.getEnvVar',
|
||||
'pm.environment.set': 'bru.setEnvVar',
|
||||
'pm.environment.name': 'bru.getEnvName()',
|
||||
'pm.environment.unset': 'bru.deleteEnvVar',
|
||||
|
||||
// Variables
|
||||
'pm.variables.get': 'bru.getVar',
|
||||
'pm.variables.set': 'bru.setVar',
|
||||
'pm.variables.has': 'bru.hasVar',
|
||||
|
||||
// Collection variables
|
||||
'pm.collectionVariables.get': 'bru.getVar',
|
||||
'pm.collectionVariables.set': 'bru.setVar',
|
||||
'pm.collectionVariables.has': 'bru.hasVar',
|
||||
'pm.collectionVariables.unset': 'bru.deleteVar',
|
||||
|
||||
// Request flow control
|
||||
'pm.setNextRequest': 'bru.setNextRequest',
|
||||
|
||||
// Testing
|
||||
'pm.test': 'test',
|
||||
'pm.expect': 'expect',
|
||||
'pm.expect.fail': 'expect.fail',
|
||||
|
||||
// Request properties
|
||||
'pm.request.url': 'req.getUrl()',
|
||||
'pm.request.method': 'req.getMethod()',
|
||||
'pm.request.headers': 'req.getHeaders()',
|
||||
'pm.request.body': 'req.getBody()',
|
||||
|
||||
// Response properties
|
||||
'pm.response.json': 'res.getBody',
|
||||
'pm.response.code': 'res.getStatus()',
|
||||
'pm.response.status': 'res.statusText',
|
||||
'pm.response.responseTime': 'res.getResponseTime()',
|
||||
'pm.response.statusText': 'res.statusText',
|
||||
'pm.response.headers': 'res.getHeaders()',
|
||||
|
||||
// Execution control
|
||||
'pm.execution.skipRequest': 'bru.runner.skipRequest',
|
||||
|
||||
// Legacy Postman API (deprecated) (we can use pm instead of postman, as we are converting all postman references to pm in the code as the part of pre-processing)
|
||||
'pm.setEnvironmentVariable': 'bru.setEnvVar',
|
||||
'pm.getEnvironmentVariable': 'bru.getEnvVar',
|
||||
'pm.clearEnvironmentVariable': 'bru.deleteEnvVar',
|
||||
};
|
||||
|
||||
/* Complex transformations that need custom handling
|
||||
* Note: Transform functions can return either a single node or an array of nodes.
|
||||
* When returning an array of nodes, each node in the array will be inserted
|
||||
* as a separate statement, which allows a single Postman expression to be
|
||||
* transformed into multiple Bruno statements (e.g. for complex assertions).
|
||||
*/
|
||||
const complexTransformations = [
|
||||
// pm.environment.has requires special handling
|
||||
{
|
||||
pattern: 'pm.environment.has',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
|
||||
const args = callExpr.arguments;
|
||||
|
||||
// Create: bru.getEnvVar(arg) !== undefined && bru.getEnvVar(arg) !== null
|
||||
return j.logicalExpression(
|
||||
'&&',
|
||||
j.binaryExpression(
|
||||
'!==',
|
||||
j.callExpression(j.identifier('bru.getEnvVar'), args),
|
||||
j.identifier('undefined')
|
||||
),
|
||||
j.binaryExpression(
|
||||
'!==',
|
||||
j.callExpression(j.identifier('bru.getEnvVar'), args),
|
||||
j.identifier('null')
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
pattern: 'pm.response.text',
|
||||
transform: (_, j) => {
|
||||
return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]);
|
||||
}
|
||||
},
|
||||
|
||||
// Handle pm.response.to.have.status
|
||||
{
|
||||
pattern: 'pm.response.to.have.status',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
|
||||
const args = callExpr.arguments;
|
||||
|
||||
// Create: expect(res.getStatus()).to.equal(arg)
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(
|
||||
j.identifier('expect'),
|
||||
[
|
||||
j.callExpression(
|
||||
j.identifier('res.getStatus'),
|
||||
[]
|
||||
)
|
||||
]
|
||||
),
|
||||
j.identifier('to.equal')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// handle 'pm.response.to.have.header' to expect(res.getHeaders()).to.have.property(args)
|
||||
{
|
||||
pattern: 'pm.response.to.have.header',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
|
||||
const args = callExpr.arguments;
|
||||
|
||||
|
||||
if (args.length > 0) {
|
||||
// Apply toLowerCase() to the first argument
|
||||
args[0] = j.callExpression(
|
||||
j.memberExpression(
|
||||
args[0],
|
||||
j.identifier('toLowerCase')
|
||||
),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
// Create: expect(res.getHeaders()).to.have.property(args)
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(
|
||||
j.identifier('expect'),
|
||||
[
|
||||
j.callExpression(
|
||||
j.identifier('res.getHeaders'),
|
||||
[]
|
||||
)
|
||||
]
|
||||
),
|
||||
j.identifier('to.have.property')
|
||||
),
|
||||
args
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Handle pm.execution.setNextRequest(null)
|
||||
{
|
||||
pattern: 'pm.execution.setNextRequest',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
|
||||
const args = callExpr.arguments;
|
||||
|
||||
// If argument is null or 'null', transform to bru.runner.stopExecution()
|
||||
if (
|
||||
args[0].type === 'Literal' && (args[0].value === null || args[0].value === 'null')
|
||||
) {
|
||||
return j.callExpression(
|
||||
j.identifier('bru.runner.stopExecution'),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, keep as bru.runner.setNextRequest with the same argument
|
||||
return j.callExpression(
|
||||
j.identifier('bru.runner.setNextRequest'),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// Create a map for complex transformations to enable O(1) lookups
|
||||
const complexTransformationsMap = {};
|
||||
complexTransformations.forEach(transform => {
|
||||
complexTransformationsMap[transform.pattern] = transform;
|
||||
});
|
||||
|
||||
const varInitsToReplace = new Set(['pm', 'postman', 'pm.request','pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution']);
|
||||
|
||||
/**
|
||||
* Process all transformations (both simple and complex) in the AST in a single pass
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
* @param {Set} transformedNodes - Set of already transformed nodes
|
||||
*/
|
||||
function processTransformations(ast, transformedNodes) {
|
||||
ast.find(j.MemberExpression).forEach(path => {
|
||||
if (transformedNodes.has(path.node)) return;
|
||||
|
||||
// Get string representation using our utility function
|
||||
const memberExprStr = getMemberExpressionString(path.value);
|
||||
|
||||
// First check for simple transformations (O(1))
|
||||
if (simpleTranslations.hasOwnProperty(memberExprStr)) {
|
||||
const replacement = simpleTranslations[memberExprStr];
|
||||
j(path).replaceWith(j.identifier(replacement));
|
||||
transformedNodes.add(path.node);
|
||||
return; // Skip complex transformation check if simple transformation applied
|
||||
}
|
||||
|
||||
// Then check for complex transformations (O(1))
|
||||
if (complexTransformationsMap.hasOwnProperty(memberExprStr) &&
|
||||
path.parent.value.type === 'CallExpression') {
|
||||
const transform = complexTransformationsMap[memberExprStr];
|
||||
const replacement = transform.transform(path, j);
|
||||
if (Array.isArray(replacement)) {
|
||||
replacement.forEach((nodePath, index) => {
|
||||
if(index === 0) {
|
||||
j(path.parent).replaceWith(nodePath);
|
||||
} else {
|
||||
j(path.parent.parent).insertAfter(nodePath);
|
||||
}
|
||||
transformedNodes.add(nodePath.node);
|
||||
transformedNodes.add(path.parent.node);
|
||||
});
|
||||
} else {
|
||||
j(path.parent).replaceWith(replacement);
|
||||
transformedNodes.add(path.node);
|
||||
transformedNodes.add(path.parent.node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates Postman script code to Bruno script code
|
||||
* @param {string} code - The Postman script code to translate
|
||||
* @returns {string} The translated Bruno script code
|
||||
*/
|
||||
function translateCode(code) {
|
||||
// Replace 'postman' with 'pm' using regex before creating the AST
|
||||
// This is more efficient than an AST traversal
|
||||
code = code.replace(/\bpostman\b/g, 'pm');
|
||||
|
||||
const ast = j(code);
|
||||
|
||||
// Keep track of transformed nodes to avoid double-processing
|
||||
const transformedNodes = new Set();
|
||||
|
||||
// Preprocess the code to resolve all aliases
|
||||
preprocessAliases(ast);
|
||||
|
||||
// Process all transformations in a single pass
|
||||
processTransformations(ast, transformedNodes);
|
||||
|
||||
// Handle special Postman syntax patterns
|
||||
handleTestsBracketNotation(ast);
|
||||
|
||||
return ast.toSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess all variable aliases in the AST to simplify later transformations
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
*/
|
||||
function preprocessAliases(ast) {
|
||||
// Create a symbol table to track what each variable references
|
||||
const symbolTable = new Map();
|
||||
const MAX_ITERATIONS = 5;
|
||||
let iterations = 0;
|
||||
|
||||
// Keep preprocessing until no more changes can be made
|
||||
let changesMade;
|
||||
do {
|
||||
changesMade = false;
|
||||
|
||||
// First pass: Identify all variables
|
||||
findVariableDefinitions(ast, symbolTable);
|
||||
|
||||
// Second pass: Replace all variable references with their resolved values
|
||||
changesMade = resolveVariableReferences(ast, symbolTable) || false;
|
||||
|
||||
// Third pass: Clean up variable declarations that are no longer needed
|
||||
changesMade = removeResolvedDeclarations(ast, symbolTable) || false;
|
||||
|
||||
iterations++;
|
||||
|
||||
} while (changesMade && iterations < MAX_ITERATIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all variable definitions and track what they reference
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
* @param {Map} symbolTable - Map to track variable references
|
||||
*/
|
||||
function findVariableDefinitions(ast, symbolTable) {
|
||||
// Use a single traversal to handle both direct assignments and object destructuring
|
||||
ast.find(j.VariableDeclarator).forEach(path => {
|
||||
// Only process nodes that have an initializer
|
||||
if (!path.value.init) return;
|
||||
|
||||
// Handle direct assignments: const response = pm.response
|
||||
if (path.value.id.type === 'Identifier') {
|
||||
const varName = path.value.id.name;
|
||||
|
||||
// If it's a direct identifier, just map it
|
||||
if (path.value.init.type === 'Identifier') {
|
||||
symbolTable.set(varName, {
|
||||
type: 'identifier',
|
||||
value: path.value.init.name
|
||||
});
|
||||
}
|
||||
// If it's a member expression, store both parts
|
||||
else if (path.value.init.type === 'MemberExpression') {
|
||||
const sourceCode = getMemberExpressionString(path.value.init);
|
||||
symbolTable.set(varName, {
|
||||
type: 'memberExpression',
|
||||
value: sourceCode,
|
||||
node: path.value.init
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handle object destructuring: const { response } = pm
|
||||
else if (path.value.id.type === 'ObjectPattern' && path.value.init.type === 'Identifier') {
|
||||
const source = path.value.init.name;
|
||||
|
||||
path.value.id.properties.forEach(prop => {
|
||||
if (prop.key.name && prop.value.type === 'Identifier') {
|
||||
const destVarName = prop.value.name;
|
||||
symbolTable.set(destVarName, {
|
||||
type: 'memberExpression',
|
||||
value: `${source}.${prop.key.name}`,
|
||||
node: j.memberExpression(
|
||||
j.identifier(source),
|
||||
j.identifier(prop.key.name)
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve variable references by replacing them with their original values
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
* @param {Map} symbolTable - Map of variable references
|
||||
* @returns {boolean} Whether any changes were made
|
||||
*/
|
||||
function resolveVariableReferences(ast, symbolTable) {
|
||||
let changesMade = false;
|
||||
|
||||
/**
|
||||
* Example of what this function does:
|
||||
*
|
||||
* Input Postman code:
|
||||
* const response = pm.response;
|
||||
* const jsonData = response.json(); // response is a reference to pm.response
|
||||
*
|
||||
* After resolution:
|
||||
* const response = pm.response;
|
||||
* const jsonData = pm.response.json(); // response reference is replaced with pm.response
|
||||
*
|
||||
* Then in the next preprocessing phase, unnecessary variables like 'response' will be removed.
|
||||
*/
|
||||
|
||||
// Replace all identifier references with their resolved values
|
||||
ast.find(j.Identifier).forEach(path => {
|
||||
const varName = path.value.name;
|
||||
|
||||
/**
|
||||
* Skip specific types of identifiers that shouldn't be replaced:
|
||||
*
|
||||
* Case 1: Variable definitions (left side of declarations)
|
||||
* -----------------------------------------------------
|
||||
* In code like:
|
||||
* const response = pm.response;
|
||||
* ^
|
||||
* We shouldn't replace 'response' on the left side with pm.response,
|
||||
* which would result in: const pm.response = pm.response; (invalid syntax)
|
||||
*
|
||||
* Case 2: Property names in member expressions
|
||||
* -----------------------------------------------------
|
||||
* In code like:
|
||||
* console.log(response.status);
|
||||
* ^
|
||||
* We shouldn't replace the 'status' property name with anything,
|
||||
* only the 'response' object reference should be replaced.
|
||||
*
|
||||
* We only want to replace identifiers that are being used as references,
|
||||
* not the ones being defined or used as property names.
|
||||
*/
|
||||
|
||||
// Skip if this is a variable definition or property name
|
||||
if (path.parent.value.type === 'VariableDeclarator' && path.parent.value.id === path.value) {
|
||||
return;
|
||||
}
|
||||
if (path.parent.value.type === 'MemberExpression' && path.parent.value.property === path.value && !path.parent.value.computed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only replace if this is a known variable
|
||||
if (!symbolTable.has(varName)) return;
|
||||
|
||||
const symbolInfo = symbolTable.get(varName);
|
||||
if(!varInitsToReplace.has(symbolInfo.value)) {
|
||||
return;
|
||||
}
|
||||
const newNode = cloneDeep(symbolInfo.node);
|
||||
j(path).replaceWith(newNode);
|
||||
symbolTable.set(varName, {
|
||||
type: 'memberExpression',
|
||||
value: symbolInfo.value,
|
||||
node: newNode
|
||||
});
|
||||
changesMade = true;
|
||||
|
||||
});
|
||||
|
||||
return changesMade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove variable declarations that have been resolved
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
* @param {Map} symbolTable - Map of variable references
|
||||
* @returns {boolean} Whether any changes were made
|
||||
*/
|
||||
function removeResolvedDeclarations(ast, symbolTable) {
|
||||
let changesMade = false;
|
||||
|
||||
/**
|
||||
* Example of what this function does:
|
||||
*
|
||||
* Original Postman code:
|
||||
* const response = pm.response;
|
||||
* const jsonData = response.json();
|
||||
* console.log(jsonData.name);
|
||||
*
|
||||
* After variable resolution:
|
||||
* const response = pm.response; // This declaration is now redundant
|
||||
* const jsonData = pm.response.json(); // This value has been resolved
|
||||
* console.log(jsonData.name); // This still references jsonData
|
||||
*
|
||||
* Final code after this cleanup step:
|
||||
* const jsonData = pm.response.json(); // response variable declaration is removed
|
||||
* console.log(jsonData.name); // jsonData is kept since it's still referenced
|
||||
*
|
||||
* We only remove declarations that:
|
||||
* 1. Have been fully resolved (references to pm.* objects)
|
||||
* 2. No longer provide any value (since all references were replaced with resolved values)
|
||||
*/
|
||||
|
||||
// Use a single traversal to handle both regular variable declarations and destructuring
|
||||
ast.find(j.VariableDeclarator).forEach(path => {
|
||||
// Case 1: Handle regular variable declarations
|
||||
if (path.value.id.type === 'Identifier') {
|
||||
const varName = path.value.id.name;
|
||||
const replacement = symbolTable.get(varName);
|
||||
if(!replacement || !varInitsToReplace.has(replacement.value)) return;
|
||||
|
||||
/**
|
||||
* This code differentiates between two types of variable declarations:
|
||||
*
|
||||
* Example 1: Single variable declaration
|
||||
* -----------------------------------
|
||||
* Input: const response = pm.response;
|
||||
* Action: The entire statement can be removed
|
||||
* Output: [statement removed]
|
||||
*
|
||||
* Example 2: Multiple variables in one declaration
|
||||
* -----------------------------------
|
||||
* Input: const response = pm.response, unrelated = 5;
|
||||
* Action: Only remove the 'response' declarator, keep the others
|
||||
* Output: const unrelated = 5;
|
||||
*
|
||||
* We need this distinction to ensure we don't accidentally remove
|
||||
* unrelated variables that happen to be declared in the same statement.
|
||||
*/
|
||||
const declarationPath = j(path).closest(j.VariableDeclaration);
|
||||
if (declarationPath.get().value.declarations.length === 1) {
|
||||
declarationPath.remove();
|
||||
} else {
|
||||
// Otherwise just remove this declarator
|
||||
j(path).remove();
|
||||
}
|
||||
|
||||
changesMade = true;
|
||||
}
|
||||
// Case 2: Handle destructuring of pm
|
||||
else if (path.value.id.type === 'ObjectPattern' &&
|
||||
path.value.init &&
|
||||
path.value.init.type === 'Identifier' &&
|
||||
path.value.init.name === 'pm') {
|
||||
|
||||
/**
|
||||
* Example of destructuring removal:
|
||||
*
|
||||
* Original Postman code:
|
||||
* const { response, environment } = pm;
|
||||
* console.log(response.json().name);
|
||||
* console.log(environment.get("variable"));
|
||||
*
|
||||
* After variable resolution steps:
|
||||
* const { response, environment } = pm; // This destructuring is now redundant
|
||||
* console.log(pm.response.json().name); // 'response' references already replaced with pm.response
|
||||
* console.log(pm.environment.get("variable")); // 'environment' references replaced
|
||||
*
|
||||
* Final code after this cleanup step:
|
||||
* console.log(pm.response.json().name); // Destructuring declaration is completely removed
|
||||
* console.log(pm.environment.get("variable"));
|
||||
*
|
||||
* This step specifically targets the Postman pattern of destructuring the pm object,
|
||||
* which is common in Postman scripts but needs to be removed in the Bruno conversion.
|
||||
*/
|
||||
|
||||
const declarationPath = j(path).closest(j.VariableDeclaration);
|
||||
if (declarationPath.get().value.declarations.length === 1) {
|
||||
declarationPath.remove();
|
||||
} else {
|
||||
j(path).remove();
|
||||
}
|
||||
|
||||
changesMade = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changesMade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Postman's tests["..."] = ... syntax
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
*/
|
||||
function handleTestsBracketNotation(ast) {
|
||||
// Find the ExpressionStatement that contains the assignment
|
||||
ast.find(j.ExpressionStatement, {
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: { name: 'tests' },
|
||||
computed: true,
|
||||
property: {} // Accept any property type
|
||||
}
|
||||
}
|
||||
}).forEach(path => {
|
||||
// Get the assignment expression
|
||||
const assignment = path.value.expression;
|
||||
const left = assignment.left;
|
||||
|
||||
// Verify it's a valid tests[] expression
|
||||
if (left.object.type === 'Identifier' &&
|
||||
left.object.name === 'tests' &&
|
||||
left.computed === true) {
|
||||
|
||||
const property = left.property;
|
||||
const rightSide = assignment.right;
|
||||
|
||||
// Handle string literals
|
||||
if (property.type === 'Literal' && typeof property.value === 'string') {
|
||||
const testName = property.value;
|
||||
|
||||
// Replace with test() function call
|
||||
j(path).replaceWith(
|
||||
j.expressionStatement(
|
||||
j.callExpression(
|
||||
j.identifier('test'),
|
||||
[
|
||||
j.literal(testName),
|
||||
j.functionExpression(
|
||||
null,
|
||||
[],
|
||||
j.blockStatement([
|
||||
j.expressionStatement(
|
||||
j.memberExpression(
|
||||
j.callExpression(
|
||||
j.identifier('expect'),
|
||||
[
|
||||
j.callExpression(
|
||||
j.identifier('Boolean'),
|
||||
[rightSide]
|
||||
)
|
||||
]
|
||||
),
|
||||
j.identifier('to.be.true')
|
||||
)
|
||||
)
|
||||
])
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
// Handle template literals
|
||||
else if (property.type === 'TemplateLiteral') {
|
||||
// Create a template literal with the same quasi and expressions
|
||||
const templateLiteral = j.templateLiteral(
|
||||
property.quasis,
|
||||
property.expressions
|
||||
);
|
||||
|
||||
// Replace with test() function call using template literal
|
||||
j(path).replaceWith(
|
||||
j.expressionStatement(
|
||||
j.callExpression(
|
||||
j.identifier('test'),
|
||||
[
|
||||
templateLiteral,
|
||||
j.functionExpression(
|
||||
null,
|
||||
[],
|
||||
j.blockStatement([
|
||||
j.expressionStatement(
|
||||
j.memberExpression(
|
||||
j.callExpression(
|
||||
j.identifier('expect'),
|
||||
[
|
||||
j.callExpression(
|
||||
j.identifier('Boolean'),
|
||||
[rightSide]
|
||||
)
|
||||
]
|
||||
),
|
||||
j.identifier('to.be.true')
|
||||
)
|
||||
)
|
||||
])
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { getMemberExpressionString };
|
||||
export default translateCode;
|
||||
@@ -0,0 +1,211 @@
|
||||
const { Worker } = require('node:worker_threads');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
function getMaxWorkers() {
|
||||
return Math.max(os.availableParallelism(), 1)
|
||||
}
|
||||
|
||||
class WorkerPool {
|
||||
constructor(scriptPath, size) {
|
||||
this.workers = [];
|
||||
this.idle = [];
|
||||
this.queue = [];
|
||||
this.scriptPath = scriptPath;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
// Initialize the worker pool
|
||||
initialize() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const worker = new Worker(this.scriptPath);
|
||||
this.workers.push(worker);
|
||||
this.idle.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Run a task on a worker
|
||||
runTask(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const task = { data, resolve, reject };
|
||||
|
||||
if (this.idle.length > 0) {
|
||||
this._runTaskOnWorker(this.idle.shift(), task);
|
||||
} else {
|
||||
this.queue.push(task);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run a task on a specific worker
|
||||
_runTaskOnWorker(workerId, task) {
|
||||
const worker = this.workers[workerId];
|
||||
|
||||
const messageHandler = (result) => {
|
||||
// Cleanup listeners
|
||||
worker.removeListener('message', messageHandler);
|
||||
worker.removeListener('error', errorHandler);
|
||||
|
||||
// Mark worker as idle
|
||||
this.idle.push(workerId);
|
||||
|
||||
// Process queue if tasks are waiting
|
||||
if (this.queue.length > 0) {
|
||||
this._runTaskOnWorker(workerId, this.queue.shift());
|
||||
}
|
||||
|
||||
// Resolve the task
|
||||
task.resolve(result);
|
||||
};
|
||||
|
||||
const errorHandler = (err) => {
|
||||
worker.removeListener('message', messageHandler);
|
||||
worker.removeListener('error', errorHandler);
|
||||
|
||||
this.idle.push(workerId);
|
||||
|
||||
if (this.queue.length > 0) {
|
||||
this._runTaskOnWorker(workerId, this.queue.shift());
|
||||
}
|
||||
|
||||
task.reject(err);
|
||||
};
|
||||
|
||||
worker.on('message', messageHandler);
|
||||
worker.on('error', errorHandler);
|
||||
worker.postMessage(task.data);
|
||||
}
|
||||
|
||||
// Terminate all workers
|
||||
terminate() {
|
||||
for (const worker of this.workers) {
|
||||
worker.terminate();
|
||||
}
|
||||
this.workers = [];
|
||||
this.idle = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to count lines in a script
|
||||
function countScriptLines(script) {
|
||||
if (!script) return 0;
|
||||
return Array.isArray(script) ? script.length : script.split('\n').length;
|
||||
}
|
||||
|
||||
// Calculate complexity of a script entry
|
||||
function calculateScriptComplexity([uid, entry]) {
|
||||
let totalLines = 0;
|
||||
const { events } = entry
|
||||
|
||||
if (events && Array.isArray(events)) {
|
||||
events.forEach(({ script }) => {
|
||||
if (script && script.exec) {
|
||||
totalLines += countScriptLines(script.exec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { uid, entry, complexity: totalLines || 1 }; // Minimum complexity of 1
|
||||
}
|
||||
|
||||
// Create balanced batches based on script complexity
|
||||
function createBalancedBatches(scriptEntries, workerCount) {
|
||||
// Calculate complexity for each script
|
||||
const scriptsWithComplexity = scriptEntries.map(calculateScriptComplexity);
|
||||
|
||||
// Sort scripts by complexity (descending)
|
||||
scriptsWithComplexity.sort((a, b) => b.complexity - a.complexity);
|
||||
|
||||
// Initialize batches
|
||||
const batches = Array.from({ length: workerCount }, () => ({
|
||||
entries: [],
|
||||
totalComplexity: 0
|
||||
}));
|
||||
|
||||
// Algorithm: Greedy load balancing
|
||||
// 1. Process scripts in descending order of complexity
|
||||
// 2. Always assign each script to the batch with lowest current load
|
||||
// 3. This minimizes the maximum workload across all workers
|
||||
for (const { uid, entry, complexity } of scriptsWithComplexity) {
|
||||
|
||||
const batchWithLowestComplexity = batches.reduce(
|
||||
(target, current) => current.totalComplexity < target.totalComplexity ? current : target
|
||||
);
|
||||
|
||||
// Add the script to this batch
|
||||
batchWithLowestComplexity.entries.push({uid, entry});
|
||||
batchWithLowestComplexity.totalComplexity += complexity;
|
||||
}
|
||||
|
||||
return batches.map(batch =>
|
||||
batch.entries.map(({ uid, entry }) => [uid, entry])
|
||||
).filter(batch => batch.length > 0);
|
||||
}
|
||||
|
||||
const scriptTranslationWorker = async (scriptMap) => {
|
||||
// Convert the Map to an array of entries
|
||||
const scriptEntries = Array.from(scriptMap.entries());
|
||||
const maxWorkers = getMaxWorkers();
|
||||
|
||||
// For very small collections, don't parallelize
|
||||
if (scriptEntries.length <= 50) {
|
||||
const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), 1);
|
||||
workerPool.initialize();
|
||||
|
||||
try {
|
||||
const translatedScripts = new Map();
|
||||
const result = await workerPool.runTask({ scripts: scriptEntries });
|
||||
|
||||
if (result.error) {
|
||||
console.error('Error in script translation worker:', result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
result.forEach(([uid, { request }]) => {
|
||||
translatedScripts.set(uid, { request });
|
||||
});
|
||||
|
||||
return translatedScripts;
|
||||
} finally {
|
||||
workerPool.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const workerCount = Math.min(maxWorkers, 4);
|
||||
|
||||
// Create balanced batches based on script complexity
|
||||
const batches = createBalancedBatches(scriptEntries, workerCount);
|
||||
|
||||
const translatedScripts = new Map();
|
||||
|
||||
// Create worker pool with optimal size
|
||||
const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), workerCount);
|
||||
workerPool.initialize();
|
||||
|
||||
// Process all batches in parallel using worker pool
|
||||
const batchPromises = batches.map(batch => {
|
||||
return workerPool.runTask({ scripts: batch })
|
||||
.then(modScripts => {
|
||||
modScripts.forEach(([name, { request }]) => {
|
||||
translatedScripts.set(name, { request });
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error in script translation worker:', err);
|
||||
throw new Error(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all batches to complete
|
||||
try {
|
||||
await Promise.allSettled(batchPromises);
|
||||
} finally {
|
||||
// Clean up worker pool
|
||||
workerPool.terminate();
|
||||
}
|
||||
|
||||
return translatedScripts;
|
||||
};
|
||||
|
||||
export default scriptTranslationWorker
|
||||
@@ -0,0 +1,44 @@
|
||||
const { parentPort } = require('node:worker_threads');
|
||||
const { postmanTranslation } = require('@usebruno/converters');
|
||||
|
||||
parentPort.on('message', (workerData) => {
|
||||
try {
|
||||
const { scripts } = workerData;
|
||||
const modScripts = scripts.map(([uid, { events }]) => {
|
||||
const requestObject = {
|
||||
script: {},
|
||||
tests: {}
|
||||
}
|
||||
|
||||
if (events && Array.isArray(events)) {
|
||||
events.forEach((event) => {
|
||||
if(event?.script && event.script.exec) {
|
||||
if(event.listen === 'prerequest') {
|
||||
if(event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.script.req = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
requestObject.script.req = '';
|
||||
}
|
||||
}
|
||||
|
||||
if(event.listen === 'test') {
|
||||
if(event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.tests = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [uid, { request: requestObject }];
|
||||
});
|
||||
|
||||
parentPort.postMessage(modScripts);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
parentPort.postMessage({ error: error?.message });
|
||||
}
|
||||
});
|
||||
@@ -32,12 +32,14 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: 'value1',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
name: 'var2',
|
||||
value: 'value2',
|
||||
enabled: false,
|
||||
secret: true,
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Collection Authentication', () => {
|
||||
it('should handle basic auth at collection level', () => {
|
||||
it('should handle basic auth at collection level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level basic auth',
|
||||
@@ -44,7 +44,7 @@ describe('Collection Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
// console.log('result', JSON.stringify(result, null, 2));
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
@@ -61,7 +61,7 @@ describe('Collection Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bearer token auth at collection level', () => {
|
||||
it('should handle bearer token auth at collection level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level bearer token',
|
||||
@@ -98,7 +98,7 @@ describe('Collection Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
// console.log('result', JSON.stringify(result, null, 2));
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
@@ -112,9 +112,9 @@ describe('Collection Authentication', () => {
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API key auth at collection level', () => {
|
||||
it('should handle API key auth at collection level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level api key',
|
||||
@@ -156,7 +156,7 @@ describe('Collection Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'apikey',
|
||||
@@ -173,7 +173,7 @@ describe('Collection Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle digest auth at collection level', () => {
|
||||
it('should handle digest auth at collection level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level digest auth',
|
||||
@@ -220,7 +220,7 @@ describe('Collection Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'digest',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Folder Authentication', () => {
|
||||
it('should handle basic auth at folder level', () => {
|
||||
it('should handle basic auth at folder level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level basic auth',
|
||||
@@ -49,7 +49,7 @@ describe('Folder Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'basic',
|
||||
@@ -65,7 +65,7 @@ describe('Folder Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bearer token auth at folder level', () => {
|
||||
it('should handle bearer token auth at folder level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level bearer token',
|
||||
@@ -107,7 +107,7 @@ describe('Folder Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
@@ -120,7 +120,7 @@ describe('Folder Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API key auth at folder level', () => {
|
||||
it('should handle API key auth at folder level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level API key',
|
||||
@@ -167,7 +167,7 @@ describe('Folder Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'apikey',
|
||||
@@ -180,7 +180,7 @@ describe('Folder Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle digest auth at folder level', () => {
|
||||
it('should handle digest auth at folder level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level digest auth',
|
||||
@@ -232,7 +232,7 @@ describe('Folder Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'digest',
|
||||
|
||||
@@ -3,7 +3,7 @@ import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('postman-collection', () => {
|
||||
it('should correctly import a valid Postman collection file', async () => {
|
||||
const brunoCollection = postmanToBruno(postmanCollection);
|
||||
const brunoCollection = await postmanToBruno(postmanCollection);
|
||||
expect(brunoCollection).toMatchObject(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -73,92 +73,93 @@ const expectedOutput = {
|
||||
"version": "1",
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "folder",
|
||||
"type": "folder",
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 1
|
||||
}
|
||||
],
|
||||
"root": {
|
||||
"docs": "",
|
||||
"meta": {
|
||||
"name": "folder"
|
||||
},
|
||||
"request": {
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"script": {},
|
||||
"tests": "",
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "folder",
|
||||
"type": "folder",
|
||||
"seq": 1,
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"seq": 1,
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"root": {
|
||||
"docs": "",
|
||||
"meta": {
|
||||
"name": "folder"
|
||||
},
|
||||
"request": {
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"script": {},
|
||||
"tests": "",
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 1
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"seq": 2,
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
}
|
||||
],
|
||||
"environments": [],
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('postmanTranslations - response commands', () => {
|
||||
const responseText = JSON.stringify(res.getBody());
|
||||
const responseJson = res.getBody();
|
||||
const responseStatus = res.statusText;
|
||||
const responseHeaders = req.getHeaders();
|
||||
const responseHeaders = res.getHeaders();
|
||||
|
||||
test('Status code is 200', function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Request Authentication', () => {
|
||||
it('should handle basic auth at request level', () => {
|
||||
it('should handle basic auth at request level', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Request Auth Collection',
|
||||
@@ -26,7 +26,7 @@ describe('Request Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].request.auth).toEqual({
|
||||
mode: 'basic',
|
||||
@@ -42,7 +42,7 @@ describe('Request Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should inherit folder auth when request has no auth', () => {
|
||||
it('should inherit folder auth when request has no auth', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Inherit Request Auth Collection',
|
||||
@@ -68,7 +68,7 @@ describe('Request Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].items[0].request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
@@ -83,7 +83,7 @@ describe('Request Authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should override folder auth with request auth', () => {
|
||||
it('should override folder auth with request auth', async() => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Override Request Auth Collection',
|
||||
@@ -116,7 +116,7 @@ describe('Request Authentication', () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
const result = await postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].items[0].request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('postmanTranslations - comment handling', () => {
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
const expectedOutput = "// test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Combined API Features Translation', () => {
|
||||
// Basic translation test
|
||||
it('should translate code', () => {
|
||||
const code = 'console.log("Hello, world!");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(code);
|
||||
});
|
||||
|
||||
// Preserving comments
|
||||
it('should preserve comments', () => {
|
||||
const code = '// This is a comment';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('// This is a comment');
|
||||
});
|
||||
|
||||
it('should preserve comments inside functions', () => {
|
||||
const code = `
|
||||
function getUserDetails() {
|
||||
// Get user details from API
|
||||
const response = pm.response.json();
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
function getUserDetails() {
|
||||
// Get user details from API
|
||||
const response = res.getBody();
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should preserve comments inside if statements', () => {
|
||||
const code = `
|
||||
if (pm.response.code === 200) {
|
||||
// Success
|
||||
console.log("Success");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
if (res.getStatus() === 200) {
|
||||
// Success
|
||||
console.log("Success");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should preserve multiline comments', () => {
|
||||
const code = `
|
||||
/*
|
||||
This is a multiline comment
|
||||
*/
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
/*
|
||||
This is a multiline comment
|
||||
*/
|
||||
`);
|
||||
});
|
||||
|
||||
it('should preserve comments inside for loops', () => {
|
||||
const code = `
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// Loop iteration
|
||||
console.log(pm.response.json()[i]);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// Loop iteration
|
||||
console.log(res.getBody()[i]);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// Multiple transformations in the same code block
|
||||
it('should handle multiple translations in the same code block', () => {
|
||||
const code = `
|
||||
const token = pm.environment.get("authToken");
|
||||
pm.test("Auth flow works", function() {
|
||||
const response = pm.response.json();
|
||||
pm.expect(response.authenticated).to.be.true;
|
||||
pm.environment.set("userId", response.user.id);
|
||||
pm.collectionVariables.set("sessionId", response.session.id);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).not.toContain('pm.test("Auth flow works", function() {');
|
||||
expect(translatedCode).not.toContain('pm.expect(response.authenticated).to.be.true;');
|
||||
expect(translatedCode).not.toContain('pm.environment.set("userId", response.user.id);');
|
||||
expect(translatedCode).not.toContain('pm.collectionVariables.set("sessionId", response.session.id);');
|
||||
expect(translatedCode).toContain('const token = bru.getEnvVar("authToken");');
|
||||
expect(translatedCode).toContain('test("Auth flow works", function() {');
|
||||
expect(translatedCode).toContain('const response = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(response.authenticated).to.be.true;');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);');
|
||||
expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);');
|
||||
});
|
||||
|
||||
// Nested expressions
|
||||
it('should handle nested Postman API calls', () => {
|
||||
const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");');
|
||||
});
|
||||
|
||||
it('should handle more complex nested expressions', () => {
|
||||
const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));');
|
||||
});
|
||||
|
||||
// Unrelated code
|
||||
it('should leave unrelated code untouched', () => {
|
||||
const code = `
|
||||
function calculateTotal(items) {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(code);
|
||||
});
|
||||
|
||||
it('should handle Postman API calls within JavaScript methods', () => {
|
||||
const code = `
|
||||
const helpers = {
|
||||
getAuthHeader: function() {
|
||||
return "Bearer " + pm.environment.get("token");
|
||||
}
|
||||
};
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('return "Bearer " + bru.getEnvVar("token");');
|
||||
});
|
||||
|
||||
|
||||
it('should handle aliases with object destructuring', () => {
|
||||
const code = `
|
||||
const { environment, variables } = pm;
|
||||
environment.set("token", "abc123");
|
||||
variables.get("userId");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
bru.setEnvVar("token", "abc123");
|
||||
bru.getVar("userId");
|
||||
`);
|
||||
});
|
||||
|
||||
// Code context tests
|
||||
it('should translate pm commands inside functions', () => {
|
||||
const code = `
|
||||
function getAuthHeader() {
|
||||
return "Bearer " + pm.environment.get("token");
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
function getAuthHeader() {
|
||||
return "Bearer " + bru.getEnvVar("token");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside if statements', () => {
|
||||
const code = `
|
||||
if (pm.response.code === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
if (res.getStatus() === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
it('should translate pm commands inside if statements', () => {
|
||||
const code = `
|
||||
const json = pm.response.json();
|
||||
if (json.code === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const json = res.getBody();
|
||||
if (json.code === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside else statements', () => {
|
||||
const code = `
|
||||
if (pm.response.code === 200) {
|
||||
console.log("Success");
|
||||
pm.response.to.have.status(200);
|
||||
} else {
|
||||
console.log("Failure");
|
||||
expect(res.getStatus()).to.equal(400);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
if (res.getStatus() === 200) {
|
||||
console.log("Success");
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
} else {
|
||||
console.log("Failure");
|
||||
expect(res.getStatus()).to.equal(400);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside for loops', () => {
|
||||
const code = `
|
||||
for (let i = 0; i < pm.response.json().length; i++) {
|
||||
console.log(pm.response.json()[i]);
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
for (let i = 0; i < res.getBody().length; i++) {
|
||||
console.log(res.getBody()[i]);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside while loops', () => {
|
||||
const code = `
|
||||
while (pm.response.code === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
while (res.getStatus() === 200) {
|
||||
console.log("Success");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside switch statements', () => {
|
||||
const code = `
|
||||
switch (pm.response.code) {
|
||||
case 200:
|
||||
console.log("Success");
|
||||
break;
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
switch (res.getStatus()) {
|
||||
case 200:
|
||||
console.log("Success");
|
||||
break;
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm commands inside try catch statements', () => {
|
||||
const code = `
|
||||
try {
|
||||
pm.response.to.have.status(200);
|
||||
} catch (error) {
|
||||
console.log("Failure");
|
||||
expect(res.getStatus()).to.equal(400);
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
try {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
} catch (error) {
|
||||
console.log("Failure");
|
||||
expect(res.getStatus()).to.equal(400);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate aliases within if statements block', () => {
|
||||
const code = `
|
||||
const env = pm.environment;
|
||||
const vars = pm.variables;
|
||||
const collVars = pm.collectionVariables;
|
||||
const test = pm.test;
|
||||
const expect = pm.expect;
|
||||
const response = pm.response;
|
||||
|
||||
function processResponse() {
|
||||
if(response.code === 200) {
|
||||
console.log("Success");
|
||||
} else if(response.code === 400) {
|
||||
console.log("Failure");
|
||||
expect(response.code).to.equal(400);
|
||||
} else {
|
||||
console.log("Unknown status code");
|
||||
expect(response.code).to.equal(500);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
function processResponse() {
|
||||
if(res.getStatus() === 200) {
|
||||
console.log("Success");
|
||||
} else if(res.getStatus() === 400) {
|
||||
console.log("Failure");
|
||||
expect(res.getStatus()).to.equal(400);
|
||||
} else {
|
||||
console.log("Unknown status code");
|
||||
expect(res.getStatus()).to.equal(500);
|
||||
}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle pm aliases inside functions', () => {
|
||||
const code = `
|
||||
const tempRes = pm.response;
|
||||
const tempTest = pm.test;
|
||||
const tempExpect = pm.expect;
|
||||
const tempEnv = pm.environment;
|
||||
const tempVars = pm.variables;
|
||||
const tempCollVars = pm.collectionVariables;
|
||||
|
||||
function processResponse() {
|
||||
tempTest("Status code is 200", function() { expect(tempRes.code).to.equal(200); });
|
||||
tempEnv.set("userId", tempRes.json().userId);
|
||||
tempVars.set("token", tempRes.json().token);
|
||||
tempCollVars.set("sessionId", tempRes.json().sessionId);
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
function processResponse() {
|
||||
test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); });
|
||||
bru.setEnvVar("userId", res.getBody().userId);
|
||||
bru.setVar("token", res.getBody().token);
|
||||
bru.setVar("sessionId", res.getBody().sessionId);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should nested pm commands', () => {
|
||||
const code = `
|
||||
pm.collectionVariables.get(pm.environment.get('key'))
|
||||
pm.test("Status code is 200", function() {
|
||||
pm.response.to.have.status(200);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.getVar(bru.getEnvVar('key'))
|
||||
test("Status code is 200", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle pm objects in template literals', () => {
|
||||
const code = `
|
||||
const baseUrl = pm.environment.get("baseUrl");
|
||||
const endpoint = pm.variables.get("endpoint");
|
||||
const url = \`\${baseUrl}/api/\${endpoint}\`;
|
||||
console.log(\`Response status: \${pm.response.code}\`);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");');
|
||||
expect(translatedCode).toContain('const endpoint = bru.getVar("endpoint");');
|
||||
expect(translatedCode).toContain('const url = `${baseUrl}/api/${endpoint}`;');
|
||||
expect(translatedCode).toContain('console.log(`Response status: ${res.getStatus()}`);');
|
||||
});
|
||||
|
||||
it('should handle pm objects in arrow functions', () => {
|
||||
const code = `
|
||||
const getAuthHeader = () => "Bearer " + pm.environment.get("token");
|
||||
const processItems = items => items.forEach(item => {
|
||||
pm.variables.set(item.key, item.value);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const getAuthHeader = () => "Bearer " + bru.getEnvVar("token");');
|
||||
expect(translatedCode).toContain('const processItems = items => items.forEach(item => {');
|
||||
expect(translatedCode).toContain('bru.setVar(item.key, item.value);');
|
||||
});
|
||||
|
||||
it('test', () => {
|
||||
const code = `
|
||||
const globals = pm.globals;
|
||||
const key = globals.get("key");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const globals = pm.globals;
|
||||
const key = globals.get("key");
|
||||
`);
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Environment Variable Translation', () => {
|
||||
it('should translate pm.environment.get', () => {
|
||||
const code = 'pm.environment.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.getEnvVar("test");');
|
||||
});
|
||||
|
||||
it('should translate pm.environment.set', () => {
|
||||
const code = 'pm.environment.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("test", "value");');
|
||||
});
|
||||
|
||||
it('should translate pm.environment.has', () => {
|
||||
const code = 'pm.environment.has("test")';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null');
|
||||
});
|
||||
|
||||
it('should translate pm.environment.unset', () => {
|
||||
const code = 'pm.environment.unset("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.deleteEnvVar("test");');
|
||||
});
|
||||
|
||||
it('should translate pm.environment.name', () => {
|
||||
const code = 'pm.environment.name;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.getEnvName();');
|
||||
});
|
||||
|
||||
it('should handle nested Postman API calls with environment', () => {
|
||||
const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");');
|
||||
});
|
||||
|
||||
it('should handle JSON operations with environment variables', () => {
|
||||
const code = 'pm.environment.set("user", JSON.stringify({ id: 123, name: "John" }));';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("user", JSON.stringify({ id: 123, name: "John" }));');
|
||||
});
|
||||
|
||||
it('should handle JSON.parse with environment variables', () => {
|
||||
const code = 'const userData = JSON.parse(pm.environment.get("user"));';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const userData = JSON.parse(bru.getEnvVar("user"));');
|
||||
});
|
||||
|
||||
it('should translate pm.environment.name with different access patterns', () => {
|
||||
const code = `
|
||||
const envName1 = pm.environment.name;
|
||||
const env = pm.environment;
|
||||
const envName2 = env.name;
|
||||
console.log(pm.environment.name);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const envName1 = bru.getEnvName();
|
||||
const envName2 = bru.getEnvName();
|
||||
console.log(bru.getEnvName());
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle environment aliases', () => {
|
||||
const code = `
|
||||
const env = pm.environment;
|
||||
const name = env.name;
|
||||
const has = env.has("test");
|
||||
const set = env.set("test", "value");
|
||||
const get = env.get("test");
|
||||
const unset = env.unset("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const name = bru.getEnvName();
|
||||
const has = bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null;
|
||||
const set = bru.setEnvVar("test", "value");
|
||||
const get = bru.getEnvVar("test");
|
||||
const unset = bru.deleteEnvVar("test");
|
||||
`);
|
||||
});
|
||||
|
||||
// Legacy API (postman.) tests related to environment
|
||||
it('should translate postman.setEnvironmentVariable', () => {
|
||||
const code = 'postman.setEnvironmentVariable("apiKey", "abc123");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("apiKey", "abc123");');
|
||||
});
|
||||
|
||||
it('should translate postman.getEnvironmentVariable', () => {
|
||||
const code = 'const baseUrl = postman.getEnvironmentVariable("baseUrl");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const baseUrl = bru.getEnvVar("baseUrl");');
|
||||
});
|
||||
|
||||
it('should translate postman.clearEnvironmentVariable', () => {
|
||||
const code = 'postman.clearEnvironmentVariable("tempToken");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.deleteEnvVar("tempToken");');
|
||||
});
|
||||
|
||||
it('should handle all environment variable methods together', () => {
|
||||
const code = `
|
||||
// All environment variable methods
|
||||
const envName = pm.environment.name;
|
||||
const hasToken = pm.environment.has("token");
|
||||
const token = pm.environment.get("token");
|
||||
pm.environment.set("timestamp", new Date().toISOString());
|
||||
|
||||
console.log(\`Environment: \${envName}, Has token: \${hasToken}, Token: \${token}\`);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const envName = bru.getEnvName();');
|
||||
expect(translatedCode).toContain('const hasToken = bru.getEnvVar("token") !== undefined && bru.getEnvVar("token") !== null;');
|
||||
expect(translatedCode).toContain('const token = bru.getEnvVar("token");');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("timestamp", new Date().toISOString());');
|
||||
});
|
||||
|
||||
// Additional robust tests for environment variables
|
||||
it('should handle environment variables with computed property names', () => {
|
||||
const code = `
|
||||
const prefix = "api";
|
||||
const suffix = "Key";
|
||||
pm.environment.set(prefix + "_" + suffix, "abc123");
|
||||
const computedValue = pm.environment.get(prefix + "_" + suffix);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('bru.setEnvVar(prefix + "_" + suffix, "abc123");');
|
||||
expect(translatedCode).toContain('const computedValue = bru.getEnvVar(prefix + "_" + suffix);');
|
||||
});
|
||||
|
||||
it('should handle environment variables in complex object structures', () => {
|
||||
const code = `
|
||||
const config = {
|
||||
baseUrl: pm.environment.get("apiUrl"),
|
||||
headers: {
|
||||
"Authorization": "Bearer " + pm.environment.get("token"),
|
||||
"X-Api-Key": pm.environment.get("apiKey") || "default-key"
|
||||
},
|
||||
timeout: parseInt(pm.environment.get("timeout") || "5000"),
|
||||
validate: pm.environment.has("validateResponses")
|
||||
};
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('baseUrl: bru.getEnvVar("apiUrl"),');
|
||||
expect(translatedCode).toContain('"Authorization": "Bearer " + bru.getEnvVar("token"),');
|
||||
expect(translatedCode).toContain('"X-Api-Key": bru.getEnvVar("apiKey") || "default-key"');
|
||||
expect(translatedCode).toContain('timeout: parseInt(bru.getEnvVar("timeout") || "5000"),');
|
||||
expect(translatedCode).toContain('validate: bru.getEnvVar("validateResponses") !== undefined && bru.getEnvVar("validateResponses") !== null');
|
||||
});
|
||||
|
||||
it('should handle environment variables in conditionals correctly', () => {
|
||||
const code = `
|
||||
if (pm.environment.has("apiKey")) {
|
||||
if (pm.environment.get("apiKey").length > 0) {
|
||||
console.log("Valid API key exists");
|
||||
} else {
|
||||
console.log("API key is empty");
|
||||
}
|
||||
} else {
|
||||
console.log("No API key defined");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) {');
|
||||
expect(translatedCode).toContain('if (bru.getEnvVar("apiKey").length > 0) {');
|
||||
});
|
||||
|
||||
it('should handle multiple levels of environment variable aliasing', () => {
|
||||
const code = `
|
||||
const env = pm.environment;
|
||||
|
||||
env.set("key", "value");
|
||||
const value = env.get("key");
|
||||
const exists = env.has("key");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.setEnvVar("key", "value");
|
||||
const value = bru.getEnvVar("key");
|
||||
const exists = bru.getEnvVar("key") !== undefined && bru.getEnvVar("key") !== null;
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle environment variables with dynamic values', () => {
|
||||
const code = `
|
||||
// Generate a timestamp for this request
|
||||
const timestamp = new Date().toISOString();
|
||||
pm.environment.set("requestTimestamp", timestamp);
|
||||
|
||||
// Generate a unique ID
|
||||
const uniqueId = "req_" + Math.random().toString(36).substring(2, 15);
|
||||
pm.environment.set("requestId", uniqueId);
|
||||
|
||||
// Calculate an expiry time (30 minutes from now)
|
||||
const expiryTime = new Date();
|
||||
expiryTime.setMinutes(expiryTime.getMinutes() + 30);
|
||||
pm.environment.set("tokenExpiry", expiryTime.getTime());
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('bru.setEnvVar("requestTimestamp", timestamp);');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("requestId", uniqueId);');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("tokenExpiry", expiryTime.getTime());');
|
||||
});
|
||||
|
||||
it('should handle environment variables in try-catch blocks', () => {
|
||||
const code = `
|
||||
try {
|
||||
const configStr = pm.environment.get("config");
|
||||
const config = JSON.parse(configStr);
|
||||
console.log("Config loaded:", config.version);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse config");
|
||||
pm.environment.set("configError", error.message);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const configStr = bru.getEnvVar("config");');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("configError", error.message);');
|
||||
});
|
||||
|
||||
it('should handle legacy environment and pm.setEnvironmentVariable together', () => {
|
||||
const code = `
|
||||
// Legacy style
|
||||
postman.setEnvironmentVariable("legacyKey", "legacyValue");
|
||||
|
||||
// Mixed with newer style
|
||||
const value = pm.environment.get("anotherKey");
|
||||
|
||||
// Another legacy form
|
||||
pm.setEnvironmentVariable("thirdKey", "thirdValue");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('bru.setEnvVar("legacyKey", "legacyValue");');
|
||||
expect(translatedCode).toContain('const value = bru.getEnvVar("anotherKey");');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("thirdKey", "thirdValue");');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Execution Flow Translation', () => {
|
||||
// Request flow control
|
||||
it('should translate pm.setNextRequest', () => {
|
||||
const code = 'pm.setNextRequest("Get User Details");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setNextRequest("Get User Details");');
|
||||
});
|
||||
|
||||
it('should translate pm.execution.skipRequest', () => {
|
||||
const code = 'if (condition) pm.execution.skipRequest();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('if (condition) bru.runner.skipRequest();');
|
||||
});
|
||||
|
||||
it('should translate pm.execution.setNextRequest(null)', () => {
|
||||
const code = 'pm.execution.setNextRequest(null);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.runner.stopExecution();');
|
||||
});
|
||||
|
||||
it('should translate pm.execution.setNextRequest("null")', () => {
|
||||
const code = 'pm.execution.setNextRequest("null");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.runner.stopExecution();');
|
||||
});
|
||||
|
||||
it('should handle pm.execution.setNextRequest with non-null parameters', () => {
|
||||
const code = `
|
||||
// Continue normal flow
|
||||
pm.execution.setNextRequest("Get user details");
|
||||
|
||||
// With variable
|
||||
const nextReq = "Update profile";
|
||||
pm.execution.setNextRequest(nextReq);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('bru.runner.setNextRequest("Get user details");');
|
||||
expect(translatedCode).toContain('bru.runner.setNextRequest(nextReq);');
|
||||
});
|
||||
|
||||
it('should handle all execution control methods together', () => {
|
||||
const code = `
|
||||
// All execution control methods
|
||||
if (pm.response.code === 401) {
|
||||
pm.execution.skipRequest();
|
||||
} else if (pm.response.code === 500) {
|
||||
pm.execution.setNextRequest(null);
|
||||
} else {
|
||||
pm.setNextRequest("Get User Details");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('if (res.getStatus() === 401) {');
|
||||
expect(translatedCode).toContain('bru.runner.skipRequest();');
|
||||
expect(translatedCode).toContain('} else if (res.getStatus() === 500) {');
|
||||
expect(translatedCode).toContain('bru.runner.stopExecution();');
|
||||
expect(translatedCode).toContain('} else {');
|
||||
expect(translatedCode).toContain('bru.setNextRequest("Get User Details");');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Legacy Tests[] Syntax Translation', () => {
|
||||
it('should handle tests[] commands', () => {
|
||||
const code = `
|
||||
tests["Status code is 200"] = pm.response.code === 200;`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Status code is 200", function() {
|
||||
expect(Boolean(res.getStatus() === 200)).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with complex expressions', () => {
|
||||
const code = `
|
||||
tests["Response has valid data"] = pm.response.json().data && pm.response.json().data.length > 0;`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Response has valid data", function() {
|
||||
expect(Boolean(res.getBody().data && res.getBody().data.length > 0)).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with string equality', () => {
|
||||
const code = `
|
||||
tests["Content-Type is application/json"] = pm.response.headers.get("Content-Type") === "application/json";`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Content-Type is application/json", function() {
|
||||
expect(Boolean(res.getHeaders().get("Content-Type") === "application/json")).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with function calls', () => {
|
||||
const code = `
|
||||
tests["Response time is acceptable"] = pm.response.responseTime < 500;`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Response time is acceptable", function() {
|
||||
expect(Boolean(res.getResponseTime() < 500)).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with variable references', () => {
|
||||
const code = `
|
||||
const expectedStatus = 201;
|
||||
tests["Status code is correct"] = pm.response.code === expectedStatus;`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const expectedStatus = 201;
|
||||
test("Status code is correct", function() {
|
||||
expect(Boolean(res.getStatus() === expectedStatus)).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle multiple tests[] statements', () => {
|
||||
const code = `
|
||||
tests["Status code is 200"] = pm.response.code === 200;
|
||||
tests["Response has data"] = pm.response.json().hasOwnProperty("data");`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Status code is 200", function() {
|
||||
expect(Boolean(res.getStatus() === 200)).to.be.true;
|
||||
});
|
||||
test("Response has data", function() {
|
||||
expect(Boolean(res.getBody().hasOwnProperty("data"))).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with special characters in name', () => {
|
||||
const code = `
|
||||
tests["Special characters: !@#$%^&*()"] = true;`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Special characters: !@#$%^&*()", function() {
|
||||
expect(Boolean(true)).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle tests[] with pm.environment variables', () => {
|
||||
const code = `
|
||||
tests["Response matches environment variable"] = pm.response.json().id === pm.environment.get("expectedId");`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Response matches environment variable", function() {
|
||||
expect(Boolean(res.getBody().id === bru.getEnvVar("expectedId"))).to.be.true;
|
||||
});`);
|
||||
});
|
||||
|
||||
it('should handle nested pm objects in tests[] assignments', () => {
|
||||
const code = `
|
||||
tests["Authentication header is present"] = pm.request.headers.has("Authorization");
|
||||
tests["Data count is correct"] = pm.response.json().items.length === pm.variables.get("expectedCount");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// The exact translation might vary depending on implementation details,
|
||||
// but we can check for key transformations
|
||||
expect(translatedCode).toContain('test("Authentication header is present"');
|
||||
expect(translatedCode).toContain('test("Data count is correct"');
|
||||
expect(translatedCode).toContain('res.getBody().items.length === bru.getVar("expectedCount")');
|
||||
});
|
||||
|
||||
// Additional robust tests for legacy tests[] syntax
|
||||
it('should handle tests[] with complex boolean expressions', () => {
|
||||
const code = `
|
||||
tests["Complex validation"] = (pm.response.code >= 200 && pm.response.code < 300) ||
|
||||
(pm.response.json().success === true && pm.response.json().data !== null);`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Complex validation", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean((res.getStatus() >= 200 && res.getStatus() < 300) ||');
|
||||
expect(translatedCode).toContain('(res.getBody().success === true && res.getBody().data !== null))).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] with array methods', () => {
|
||||
const code = `
|
||||
tests["All items have an ID"] = pm.response.json().items.every(item => item.hasOwnProperty('id'));
|
||||
tests["Has premium item"] = pm.response.json().items.some(item => item.type === 'premium');`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("All items have an ID", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getBody().items.every(item => item.hasOwnProperty(\'id\')))).to.be.true;');
|
||||
expect(translatedCode).toContain('test("Has premium item", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getBody().items.some(item => item.type === \'premium\'))).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] with template literals in the name', () => {
|
||||
const code = `
|
||||
const endpoint = "users";
|
||||
tests[\`Endpoint \${endpoint} returns valid response\`] = pm.response.code === 200;`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const endpoint = "users";');
|
||||
expect(translatedCode).toContain('test(`Endpoint ${endpoint} returns valid response`, function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] with deep property access', () => {
|
||||
const code = `
|
||||
tests["User has admin role"] = pm.response.json().user &&
|
||||
pm.response.json().user.roles &&
|
||||
pm.response.json().user.roles.includes('admin');`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("User has admin role", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getBody().user &&');
|
||||
expect(translatedCode).toContain('res.getBody().user.roles &&');
|
||||
expect(translatedCode).toContain('res.getBody().user.roles.includes(\'admin\'))).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] with JSON schema validation patterns', () => {
|
||||
const code = `
|
||||
const schema = {
|
||||
type: "object",
|
||||
required: ["id", "name"],
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
name: { type: "string" }
|
||||
}
|
||||
};
|
||||
|
||||
const data = pm.response.json();
|
||||
|
||||
// Basic schema validation patterns
|
||||
tests["Has required fields"] = data.hasOwnProperty('id') && data.hasOwnProperty('name');
|
||||
tests["ID is string"] = typeof data.id === 'string';
|
||||
tests["Name is string"] = typeof data.name === 'string';`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const schema = {');
|
||||
expect(translatedCode).toContain('type: "object",');
|
||||
expect(translatedCode).toContain('required: ["id", "name"],');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('test("Has required fields", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'id\') && data.hasOwnProperty(\'name\'))).to.be.true;');
|
||||
expect(translatedCode).toContain('test("ID is string", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(typeof data.id === \'string\')).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] within conditional blocks', () => {
|
||||
const code = `
|
||||
const data = pm.response.json();
|
||||
|
||||
if (pm.response.code === 200) {
|
||||
tests["Success response has data"] = data.hasOwnProperty('items');
|
||||
|
||||
if (data.items.length > 0) {
|
||||
tests["First item has ID"] = data.items[0].hasOwnProperty('id');
|
||||
}
|
||||
} else {
|
||||
tests["Error response has message"] = data.hasOwnProperty('message');
|
||||
}`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('if (res.getStatus() === 200) {');
|
||||
expect(translatedCode).toContain('test("Success response has data", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'items\'))).to.be.true;');
|
||||
expect(translatedCode).toContain('if (data.items.length > 0) {');
|
||||
expect(translatedCode).toContain('test("First item has ID", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(data.items[0].hasOwnProperty(\'id\'))).to.be.true;');
|
||||
expect(translatedCode).toContain('} else {');
|
||||
expect(translatedCode).toContain('test("Error response has message", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'message\'))).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle tests[] with combination of legacy and modern styles', () => {
|
||||
const code = `
|
||||
// Legacy style
|
||||
tests["Status code is 200"] = pm.response.code === 200;
|
||||
|
||||
// Modern style
|
||||
pm.test("Response has valid data", function() {
|
||||
const json = pm.response.json();
|
||||
pm.expect(json).to.be.an('object');
|
||||
pm.expect(json.items).to.be.an('array');
|
||||
|
||||
// Mix by using tests[] inside pm.test
|
||||
tests["All items have price"] = json.items.every(item => item.hasOwnProperty('price'));
|
||||
});`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Status code is 200", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');
|
||||
expect(translatedCode).toContain('test("Response has valid data", function() {');
|
||||
expect(translatedCode).toContain('const json = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(json).to.be.an(\'object\');');
|
||||
expect(translatedCode).toContain('expect(json.items).to.be.an(\'array\');');
|
||||
expect(translatedCode).toContain('test("All items have price", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(json.items.every(item => item.hasOwnProperty(\'price\')))).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle complex real-world tests[] example', () => {
|
||||
const code = `
|
||||
// Parse response
|
||||
const response = pm.response.json();
|
||||
|
||||
// Basic response validation
|
||||
tests["Status code is 200"] = pm.response.code === 200;
|
||||
tests["Response is valid JSON"] = response !== null && typeof response === 'object';
|
||||
|
||||
// Check headers
|
||||
tests["Has content-type header"] = pm.response.headers.has("Content-Type");
|
||||
tests["Content-Type is JSON"] = pm.response.headers.get("Content-Type").includes("application/json");
|
||||
|
||||
// Validate against expected values
|
||||
const expectedItems = parseInt(pm.environment.get("expectedItemCount"));
|
||||
tests["Has correct number of items"] = response.items.length === expectedItems;
|
||||
|
||||
// Check for required fields on all items
|
||||
const requiredFields = ["id", "name", "price", "category"];
|
||||
tests["All items have required fields"] = response.items.every(item => {
|
||||
return requiredFields.every(field => item.hasOwnProperty(field));
|
||||
});
|
||||
|
||||
// Validate specific business rules
|
||||
tests["No items with zero price"] = response.items.every(item => parseFloat(item.price) > 0);
|
||||
tests["Has at least one featured item"] = response.items.some(item => item.featured === true);
|
||||
|
||||
// If we find a specific item we're looking for, save its ID for later
|
||||
const targetItem = response.items.find(item => item.name === pm.variables.get("targetItemName"));
|
||||
if (targetItem) {
|
||||
pm.environment.set("targetItemId", targetItem.id);
|
||||
tests["Found target item"] = true;
|
||||
}`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check key transformations
|
||||
expect(translatedCode).toContain('const response = res.getBody();');
|
||||
expect(translatedCode).toContain('test("Status code is 200", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');
|
||||
expect(translatedCode).toContain('test("Has content-type header", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getHeaders().has("Content-Type"))).to.be.true;');
|
||||
expect(translatedCode).toContain('test("Content-Type is JSON", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(res.getHeaders().get("Content-Type").includes("application/json"))).to.be.true;');
|
||||
expect(translatedCode).toContain('const expectedItems = parseInt(bru.getEnvVar("expectedItemCount"));');
|
||||
expect(translatedCode).toContain('test("Has correct number of items", function() {');
|
||||
expect(translatedCode).toContain('expect(Boolean(response.items.length === expectedItems)).to.be.true;');
|
||||
expect(translatedCode).toContain('const targetItem = response.items.find(item => item.name === bru.getVar("targetItemName"));');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("targetItemId", targetItem.id);');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Multiline Syntax Handling', () => {
|
||||
it('should handle basic multiline variable syntax with indentation', () => {
|
||||
const code = `
|
||||
const userId = pm.variables
|
||||
.get("userId");
|
||||
pm.variables
|
||||
.set("timestamp", new Date().toISOString());
|
||||
const hasToken = pm.variables
|
||||
.has("token");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const userId = bru.getVar("userId");
|
||||
bru.setVar("timestamp", new Date().toISOString());
|
||||
const hasToken = bru.hasVar("token");
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle multiline environment variable syntax', () => {
|
||||
const code = `
|
||||
const baseUrl = pm
|
||||
.environment
|
||||
.get("baseUrl");
|
||||
pm
|
||||
.environment
|
||||
.set("requestTime", Date.now());
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const baseUrl = bru.getEnvVar("baseUrl");
|
||||
bru.setEnvVar("requestTime", Date.now());
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle multiline collection variable syntax', () => {
|
||||
const code = `
|
||||
const apiKey = pm.collectionVariables
|
||||
.get("apiKey");
|
||||
pm.collectionVariables
|
||||
.set("lastRun", new Date().toISOString());
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const apiKey = bru.getVar("apiKey");
|
||||
bru.setVar("lastRun", new Date().toISOString());
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex environment.has transformation with multiline syntax', () => {
|
||||
const code = `
|
||||
if (pm.environment
|
||||
.has("apiKey")) {
|
||||
console.log("API Key exists");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) {
|
||||
console.log("API Key exists");
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle response.to.have.status with multiline formatting', () => {
|
||||
const code = `
|
||||
pm.test("Status code is correct", function() {
|
||||
pm
|
||||
.response
|
||||
.to
|
||||
.have
|
||||
.status(200);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');
|
||||
});
|
||||
|
||||
it('should handle response.to.have.header with multiline formatting', () => {
|
||||
const code = `
|
||||
pm.test("Content type is present", function() {
|
||||
pm
|
||||
.response
|
||||
.to
|
||||
.have
|
||||
.header("content-type");
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())');
|
||||
});
|
||||
|
||||
it('should handle response properties with multiline syntax', () => {
|
||||
const code = `
|
||||
const responseBody = pm
|
||||
.response
|
||||
.json();
|
||||
const responseText = pm
|
||||
.response
|
||||
.text;
|
||||
const responseTime = pm
|
||||
.response
|
||||
.responseTime;
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const responseBody = res.getBody()');
|
||||
expect(translatedCode).toContain('const responseText = ');
|
||||
expect(translatedCode).toContain('const responseTime = res.getResponseTime()');
|
||||
});
|
||||
|
||||
it('should handle execution flow control with multiline syntax', () => {
|
||||
const code = `
|
||||
// Stop execution
|
||||
pm
|
||||
.execution
|
||||
.setNextRequest(null);
|
||||
|
||||
// Continue to next request
|
||||
pm
|
||||
.execution
|
||||
.setNextRequest("Next API Call");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('// Stop execution');
|
||||
expect(translatedCode).toContain('// Continue to next request');
|
||||
expect(translatedCode).toContain('bru.runner.stopExecution()');
|
||||
expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")');
|
||||
});
|
||||
|
||||
it('should handle mixed normal and multiline syntax in the same code', () => {
|
||||
const code = `
|
||||
// Normal syntax
|
||||
const normalVar = pm.variables.get("normal");
|
||||
|
||||
// Multiline syntax
|
||||
const multilineVar = pm.variables
|
||||
.get("multiline");
|
||||
|
||||
// Normal syntax again
|
||||
pm.variables.set("normalSet", "value");
|
||||
|
||||
// Multiline syntax again
|
||||
pm.variables
|
||||
.set("multilineSet", "value");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
// Normal syntax
|
||||
const normalVar = bru.getVar("normal");
|
||||
|
||||
// Multiline syntax
|
||||
const multilineVar = bru.getVar("multiline");
|
||||
|
||||
// Normal syntax again
|
||||
bru.setVar("normalSet", "value");
|
||||
|
||||
// Multiline syntax again
|
||||
bru.setVar("multilineSet", "value");
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex multiline method chaining', () => {
|
||||
const code = `
|
||||
pm
|
||||
.test("Test with chaining", function() {
|
||||
pm
|
||||
.response
|
||||
.to
|
||||
.have
|
||||
.status(200);
|
||||
|
||||
const body = pm
|
||||
.response
|
||||
.json();
|
||||
|
||||
pm
|
||||
.expect(body)
|
||||
.to
|
||||
.have
|
||||
.property('success')
|
||||
.equal(true);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('test("Test with chaining", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');
|
||||
expect(translatedCode).toContain('const body = res.getBody()');
|
||||
expect(translatedCode).toContain('.property(\'success\')');
|
||||
expect(translatedCode).toContain('.equal(true)');
|
||||
});
|
||||
|
||||
it('should handle a comprehensive script with various multiline formats', () => {
|
||||
const code = `
|
||||
// This comprehensive script tests different multiline styles and whitespace variations
|
||||
|
||||
// Environment variables with different formatting styles
|
||||
const baseUrl = pm.environment.get("baseUrl");
|
||||
const apiKey = pm
|
||||
.environment
|
||||
.get("apiKey");
|
||||
const userId = pm.environment
|
||||
.get("userId");
|
||||
|
||||
// Mix of variable styles
|
||||
pm.variables.set("testId", "test-" + Date.now());
|
||||
pm
|
||||
.variables
|
||||
.set("timestamp", new Date().toISOString());
|
||||
|
||||
// Collection variables with inconsistent spacing
|
||||
pm.collectionVariables
|
||||
.set("lastRun", new Date());
|
||||
|
||||
// Complex conditionals with multiline expressions
|
||||
if (pm
|
||||
.environment
|
||||
.has("apiKey") &&
|
||||
pm.variables.has("testId")) {
|
||||
|
||||
// Testing response with mixed syntax styles
|
||||
pm.test("Response validation", function() {
|
||||
// Normal style
|
||||
pm.response.to.have.status(200);
|
||||
|
||||
// Multiline with different indentation
|
||||
pm
|
||||
.response
|
||||
.to
|
||||
.have
|
||||
.header("content-type");
|
||||
|
||||
pm.response
|
||||
.to.have
|
||||
.jsonBody("success", true);
|
||||
|
||||
// Extreme indentation
|
||||
pm
|
||||
.response
|
||||
.to
|
||||
.not
|
||||
.have
|
||||
.jsonBody("error");
|
||||
});
|
||||
|
||||
// Flow control with mixed styles
|
||||
if (pm.response.code === 401) {
|
||||
pm.execution.setNextRequest(null);
|
||||
} else {
|
||||
pm
|
||||
.execution
|
||||
.setNextRequest("Next API Call");
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl")');
|
||||
expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey")');
|
||||
expect(translatedCode).toContain('const userId = bru.getEnvVar("userId")');
|
||||
|
||||
// Check variables translations
|
||||
expect(translatedCode).toContain('bru.setVar("testId", "test-" + Date.now())');
|
||||
expect(translatedCode).toContain('bru.setVar("timestamp", new Date().toISOString())');
|
||||
|
||||
// Check collection variables
|
||||
expect(translatedCode).toContain('bru.setVar("lastRun", new Date())');
|
||||
|
||||
// Check complex conditionals
|
||||
expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null &&');
|
||||
expect(translatedCode).toContain('bru.hasVar("testId"))');
|
||||
|
||||
// Check response testing
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())');
|
||||
|
||||
// Check flow control
|
||||
expect(translatedCode).toContain('if (res.getStatus() === 401)');
|
||||
expect(translatedCode).toContain('bru.runner.stopExecution()');
|
||||
expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Postman to PM References Conversion', () => {
|
||||
// Basic conversions
|
||||
it('should convert basic postman references to pm', () => {
|
||||
const code = 'postman.setEnvironmentVariable("key", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setEnvVar("key", "value");');
|
||||
// The key part is that it should convert postman.* to pm.* internally before
|
||||
// translating to bru.* APIs
|
||||
});
|
||||
|
||||
it('should convert postman variable access to pm', () => {
|
||||
const code = 'const value = postman.getEnvironmentVariable("key");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const value = bru.getEnvVar("key");');
|
||||
});
|
||||
|
||||
it('should handle postman variable assignments', () => {
|
||||
const code = `
|
||||
const envVar = postman.environment.get("apiKey");
|
||||
const baseUrl = postman.environment.get("baseUrl");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const envVar = bru.getEnvVar("apiKey");');
|
||||
expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");');
|
||||
});
|
||||
|
||||
// More complex patterns
|
||||
it('should handle mixed postman and pm references in the same code', () => {
|
||||
const code = `
|
||||
// Using both postman and pm APIs
|
||||
const apiKey = postman.environment.get("apiKey");
|
||||
const baseUrl = pm.environment.get("baseUrl");
|
||||
|
||||
// Using both formats in a test
|
||||
postman.test("Status code is 200", function() {
|
||||
pm.expect(pm.response.code).to.equal(200);
|
||||
});
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");');
|
||||
expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");');
|
||||
expect(translatedCode).toContain('test("Status code is 200", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
});
|
||||
|
||||
it('should handle postman references in object destructuring', () => {
|
||||
const code = `
|
||||
const { environment } = postman;
|
||||
environment.set("key", "value");
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('bru.setEnvVar("key", "value");');
|
||||
});
|
||||
|
||||
// Complex control flows
|
||||
it('should handle postman references in control flow statements', () => {
|
||||
const code = `
|
||||
if (postman.environment.get("isProduction") === "true") {
|
||||
const apiUrl = postman.environment.get("prodUrl");
|
||||
postman.setNextRequest("Production Flow");
|
||||
} else {
|
||||
const apiUrl = postman.environment.get("devUrl");
|
||||
postman.setNextRequest("Development Flow");
|
||||
}
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('if (bru.getEnvVar("isProduction") === "true") {');
|
||||
expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("prodUrl");');
|
||||
expect(translatedCode).toContain('bru.setNextRequest("Production Flow");');
|
||||
expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("devUrl");');
|
||||
expect(translatedCode).toContain('bru.setNextRequest("Development Flow");');
|
||||
});
|
||||
|
||||
// Legacy response handling
|
||||
it('should handle legacy postman response methods', () => {
|
||||
const code = `
|
||||
// Using legacy response handling
|
||||
const responseCode = postman.response.code;
|
||||
const responseBody = postman.response.json();
|
||||
|
||||
// Set environment variables with response data
|
||||
postman.setEnvironmentVariable("lastResponseCode", responseCode);
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const responseCode = res.getStatus();');
|
||||
expect(translatedCode).toContain('const responseBody = res.getBody();');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("lastResponseCode", responseCode);');
|
||||
});
|
||||
|
||||
// Postman in string literals should be untouched
|
||||
it('should not convert postman references in string literals', () => {
|
||||
const code = `
|
||||
console.log("This is a pm script");
|
||||
const message = "We're using pm to test our API";
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('console.log("This is a pm script");');
|
||||
expect(translatedCode).toContain('const message = "We\'re using pm to test our API";');
|
||||
});
|
||||
|
||||
// Complex example with aliasing
|
||||
it('should handle complex postman reference patterns with aliasing', () => {
|
||||
const code = `
|
||||
// Aliasing the postman object
|
||||
const env = postman.environment;
|
||||
const code = postman.code;
|
||||
|
||||
// Using the alias
|
||||
const apiKey = env.get("apiKey");
|
||||
const userId = env.get("userId");
|
||||
|
||||
// Using alias in tests
|
||||
postman.test("Response is valid", function() {
|
||||
postman.expect(code).to.equal(200);
|
||||
});
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
// Should handle the aliases properly
|
||||
expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");');
|
||||
expect(translatedCode).toContain('const userId = bru.getEnvVar("userId");');
|
||||
expect(translatedCode).toContain('test("Response is valid", function() {');
|
||||
expect(translatedCode).toContain('expect(code).to.equal(200);');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Request Translation', () => {
|
||||
it('should translate pm.request.url', () => {
|
||||
const code = 'const requestUrl = pm.request.url;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const requestUrl = req.getUrl();');
|
||||
});
|
||||
|
||||
it('should translate pm.request.method', () => {
|
||||
const code = 'const method = pm.request.method;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const method = req.getMethod();');
|
||||
});
|
||||
|
||||
it('should translate pm.request.headers', () => {
|
||||
const code = 'const headers = pm.request.headers;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const headers = req.getHeaders();');
|
||||
});
|
||||
|
||||
it('should translate pm.request.body', () => {
|
||||
const code = 'const body = pm.request.body;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const body = req.getBody();');
|
||||
});
|
||||
|
||||
it('should translate pm.response.statusText', () => {
|
||||
const code = 'const statusText = pm.response.statusText;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const statusText = res.statusText;');
|
||||
});
|
||||
|
||||
it('should translate multiple request methods in one block', () => {
|
||||
const code = `
|
||||
const url = pm.request.url;
|
||||
const method = pm.request.method;
|
||||
const headers = pm.request.headers;
|
||||
const body = pm.request.body;
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const url = req.getUrl();
|
||||
const method = req.getMethod();
|
||||
const headers = req.getHeaders();
|
||||
const body = req.getBody();
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle request and response properties together', () => {
|
||||
const code = `
|
||||
// Get request data
|
||||
const url = pm.request.url;
|
||||
const method = pm.request.method;
|
||||
|
||||
// Get response data
|
||||
const statusCode = pm.response.code;
|
||||
const statusText = pm.response.statusText;
|
||||
|
||||
// Verify expectations
|
||||
pm.test("Request was made correctly", function() {
|
||||
pm.expect(method).to.equal("POST");
|
||||
pm.expect(url).to.include("/api/items");
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const url = req.getUrl();');
|
||||
expect(translatedCode).toContain('const method = req.getMethod();');
|
||||
expect(translatedCode).toContain('const statusCode = res.getStatus();');
|
||||
expect(translatedCode).toContain('const statusText = res.statusText;');
|
||||
expect(translatedCode).toContain('test("Request was made correctly", function() {');
|
||||
expect(translatedCode).toContain('expect(method).to.equal("POST");');
|
||||
expect(translatedCode).toContain('expect(url).to.include("/api/items");');
|
||||
});
|
||||
|
||||
it('should handle request properties in conditional blocks', () => {
|
||||
const code = `
|
||||
if (pm.request.method === "POST") {
|
||||
console.log("This is a POST request to " + pm.request.url);
|
||||
pm.test("Request has correct content-type", function() {
|
||||
pm.expect(pm.request.headers.has("Content-Type")).to.be.true;
|
||||
});
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('if (req.getMethod() === "POST") {');
|
||||
expect(translatedCode).toContain('console.log("This is a POST request to " + req.getUrl());');
|
||||
expect(translatedCode).toContain('test("Request has correct content-type", function() {');
|
||||
// Note: The expectation for headers.has might be transformed differently
|
||||
// depending on how complex transformations are handled
|
||||
});
|
||||
|
||||
it('should handle request data extraction and variable setting', () => {
|
||||
const code = `
|
||||
// Extract request data
|
||||
const requestData = pm.request.body;
|
||||
const contentType = pm.request.headers.get("Content-Type");
|
||||
|
||||
// Save for later use
|
||||
pm.variables.set("lastRequestBody", JSON.stringify(requestData));
|
||||
pm.environment.set("lastContentType", contentType);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const requestData = req.getBody();');
|
||||
expect(translatedCode).toContain('bru.setVar("lastRequestBody", JSON.stringify(requestData));');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("lastContentType", contentType);');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,489 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Response Translation', () => {
|
||||
// Basic response property tests
|
||||
it('should translate pm.response.json', () => {
|
||||
const code = 'const jsonData = pm.response.json();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const jsonData = res.getBody();');
|
||||
});
|
||||
|
||||
it('should translate pm.response.code', () => {
|
||||
const code = 'if (pm.response.code === 200) { console.log("Success"); }';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('if (res.getStatus() === 200) { console.log("Success"); }');
|
||||
});
|
||||
|
||||
it('should translate pm.response.text', () => {
|
||||
const code = 'const responseText = pm.response.text();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const responseText = JSON.stringify(res.getBody());');
|
||||
});
|
||||
|
||||
it('should translate pm.response.responseTime', () => {
|
||||
const code = 'console.log("Response time:", pm.response.responseTime);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('console.log("Response time:", res.getResponseTime());');
|
||||
});
|
||||
|
||||
it('should translate pm.response.statusText', () => {
|
||||
const code = 'console.log("Status text:", pm.response.statusText);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('console.log("Status text:", res.statusText);');
|
||||
});
|
||||
|
||||
// Complex response transformations
|
||||
it('should transform pm.response.to.have.status', () => {
|
||||
const code = 'pm.response.to.have.status(201);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getStatus()).to.equal(201);');
|
||||
});
|
||||
|
||||
it('should transform pm.response.to.have.header with single argument', () => {
|
||||
const code = 'pm.response.to.have.header("Content-Type");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase());');
|
||||
});
|
||||
|
||||
it('should transform multiple pm.response.to.have.header statements', () => {
|
||||
const code = `
|
||||
pm.response.to.have.header("Content-Type", "application/json");
|
||||
pm.response.to.have.header("Cache-Control", "no-cache");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check for the existence of all four assertions (two pairs)
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase(), "no-cache");');
|
||||
});
|
||||
|
||||
it('should transform pm.response.to.have.header inside control structures', () => {
|
||||
const code = `
|
||||
if (pm.response.code === 200) {
|
||||
pm.response.to.have.header("Content-Type", "application/json");
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// The assertions should be inside the if block
|
||||
expect(translatedCode).toContain('if (res.getStatus() === 200) {');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");');
|
||||
});
|
||||
|
||||
it('should transform pm.response.to.have.header with variable parameters', () => {
|
||||
const code = `
|
||||
const headerName = "Content-Type";
|
||||
const expectedValue = "application/json";
|
||||
pm.response.to.have.header(headerName, expectedValue);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const headerName = "Content-Type";');
|
||||
expect(translatedCode).toContain('const expectedValue = "application/json";');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);');
|
||||
});
|
||||
|
||||
// Response aliases tests
|
||||
it('should handle response aliases', () => {
|
||||
const code = `
|
||||
const response = pm.response;
|
||||
const status = response.status;
|
||||
const body = response.json();
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const status = res.statusText;
|
||||
const body = res.getBody();
|
||||
`);
|
||||
});
|
||||
|
||||
// Response to.have.status with different formats
|
||||
it('should handle pm.response.to.have.status with different status codes', () => {
|
||||
const code = `
|
||||
// Test different status codes
|
||||
pm.response.to.have.status(200); // OK
|
||||
pm.response.to.have.status(201); // Created
|
||||
pm.response.to.have.status(400); // Bad Request
|
||||
pm.response.to.have.status(404); // Not Found
|
||||
pm.response.to.have.status(500); // Server Error
|
||||
|
||||
// With variables
|
||||
const expectedStatus = 200;
|
||||
pm.response.to.have.status(expectedStatus);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(201);');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(400);');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(404);');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(500);');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(expectedStatus);');
|
||||
});
|
||||
|
||||
// Alias for pm.response.to.have.status
|
||||
it('should handle pm.response.to.have.status alias', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
resp.to.have.status(200);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle pm.response.to.have.header alias', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
resp.to.have.header("Content-Type");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase());
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle pm.response.to.have.header alias with value check', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
resp.to.have.header("Content-Type", "application/json");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check for both assertions when using an alias
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");');
|
||||
});
|
||||
|
||||
|
||||
it('should translate response.status', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
const statusCode = resp.status;
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const statusCode = res.statusText;
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate response.body', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
const responseBody = resp.json();
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const responseBody = res.getBody();
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate pm.response.statusText', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
const statusText = resp.statusText;
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const statusText = res.statusText;
|
||||
`);
|
||||
});
|
||||
|
||||
it('should translate multiple response methods in one block', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
const statusCode = resp.code;
|
||||
const statusText = resp.statusText;
|
||||
const jsonData = resp.json();
|
||||
const responseText = resp.text();
|
||||
const time = resp.responseTime;
|
||||
resp.to.have.status(200);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const statusCode = res.getStatus();
|
||||
const statusText = res.statusText;
|
||||
const jsonData = res.getBody();
|
||||
const responseText = JSON.stringify(res.getBody());
|
||||
const time = res.getResponseTime();
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle accessing nested properties on response objects', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
const data = resp.json();
|
||||
if (data && data.user && data.user.id) {
|
||||
pm.environment.set("userId", data.user.id);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).not.toContain('const resp = pm.response;');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userId", data.user.id);');
|
||||
});
|
||||
|
||||
it('should handle all response property methods together', () => {
|
||||
const code = `
|
||||
// All response property methods
|
||||
const statusCode = pm.response.code;
|
||||
const responseBody = pm.response.json();
|
||||
const responseText = pm.response.text();
|
||||
const statusText = pm.response.statusText;
|
||||
const responseTime = pm.response.responseTime;
|
||||
|
||||
pm.test("Response is valid", function() {
|
||||
pm.response.to.have.status(200);
|
||||
pm.expect(responseBody).to.be.an('object');
|
||||
pm.expect(responseTime).to.be.below(1000);
|
||||
pm.expect(statusText).to.equal('OK');
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const statusCode = res.getStatus();');
|
||||
expect(translatedCode).toContain('const responseBody = res.getBody();');
|
||||
expect(translatedCode).toContain('const responseText = JSON.stringify(res.getBody());');
|
||||
expect(translatedCode).toContain('const responseTime = res.getResponseTime();');
|
||||
expect(translatedCode).toContain('const statusText = res.statusText;');
|
||||
expect(translatedCode).toContain('test("Response is valid", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
expect(translatedCode).toContain('expect(responseBody).to.be.an(\'object\');');
|
||||
expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);');
|
||||
expect(translatedCode).toContain('expect(statusText).to.equal(\'OK\');');
|
||||
});
|
||||
|
||||
it('should handle pm objects with array access on response', () => {
|
||||
const code = `
|
||||
const items = pm.response.json().items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
pm.collectionVariables.set("item_" + i, items[i].id);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const items = res.getBody().items;');
|
||||
expect(translatedCode).toContain('bru.setVar("item_" + i, items[i].id);');
|
||||
});
|
||||
|
||||
it('should handle response JSON with optional chaining and nullish coalescing', () => {
|
||||
const code = `
|
||||
const userId = pm.response.json()?.user?.id ?? "anonymous";
|
||||
const items = pm.response.json()?.data?.items || [];
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const userId = res.getBody()?.user?.id ?? "anonymous";');
|
||||
expect(translatedCode).toContain('const items = res.getBody()?.data?.items || [];');
|
||||
});
|
||||
|
||||
it('should handle response headers with different access patterns', () => {
|
||||
// will need to handle get, set methods, bruno does not support this yet
|
||||
const code = `
|
||||
const contentType = pm.response.headers.get('Content-Type');
|
||||
const contentLength = pm.response.headers.get('Content-Length');
|
||||
console.log("contentType", contentType);
|
||||
console.log("contentLength", contentLength);
|
||||
|
||||
pm.test("Headers are correct", function() {
|
||||
pm.response.to.have.header('Content-Type');
|
||||
pm.response.to.have.header('Content-Length');
|
||||
pm.expect(contentType).to.include('application/json');
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check how header access is translated
|
||||
expect(translatedCode).toContain('const contentType = res.getHeaders().get(\'Content-Type\');');
|
||||
expect(translatedCode).toContain('const contentLength = res.getHeaders().get(\'Content-Length\');');
|
||||
expect(translatedCode).toContain('console.log("contentType", contentType);');
|
||||
expect(translatedCode).toContain('console.log("contentLength", contentLength);');
|
||||
expect(translatedCode).not.toContain('pm.test')
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Type\'.toLowerCase())');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Length\'.toLowerCase())');
|
||||
expect(translatedCode).toContain('expect(contentType).to.include(\'application/json\')');
|
||||
});
|
||||
|
||||
it('should transform response data with array destructuring', () => {
|
||||
const code = `
|
||||
const { id, name, items } = pm.response.json();
|
||||
const [first, second] = items;
|
||||
pm.environment.set("userId", id);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const { id, name, items } = res.getBody();');
|
||||
expect(translatedCode).toContain('const [first, second] = items;');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userId", id);');
|
||||
});
|
||||
|
||||
it('should handle response in complex conditionals', () => {
|
||||
const code = `
|
||||
if (pm.response.code >= 200 && pm.response.code < 300) {
|
||||
if (pm.response.headers.get('Content-Type').includes('application/json')) {
|
||||
const data = pm.response.json();
|
||||
|
||||
if (data.success === true && data.token) {
|
||||
pm.environment.set("authToken", data.token);
|
||||
} else if (data.error) {
|
||||
console.error("API error:", data.error);
|
||||
}
|
||||
}
|
||||
} else if (pm.response.code === 404) {
|
||||
console.log("Resource not found");
|
||||
} else {
|
||||
console.error("Request failed with status:", pm.response.code);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('if (res.getStatus() >= 200 && res.getStatus() < 300) {');
|
||||
expect(translatedCode).toContain('if (res.getHeaders().get(\'Content-Type\').includes(\'application/json\')) {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("authToken", data.token);');
|
||||
expect(translatedCode).toContain('} else if (res.getStatus() === 404) {');
|
||||
expect(translatedCode).toContain('console.error("Request failed with status:", res.getStatus());');
|
||||
});
|
||||
|
||||
it('should handle response processing with try-catch', () => {
|
||||
const code = `
|
||||
try {
|
||||
const data = pm.response.json();
|
||||
pm.environment.set("userData", JSON.stringify(data.user));
|
||||
} catch (error) {
|
||||
console.error("Failed to parse response:", error);
|
||||
const text = pm.response.text();
|
||||
pm.environment.set("rawResponse", text);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userData", JSON.stringify(data.user));');
|
||||
expect(translatedCode).toContain('const text = JSON.stringify(res.getBody());');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("rawResponse", text);');
|
||||
});
|
||||
|
||||
it('should handle JSON path style access to response data', () => {
|
||||
const code = `
|
||||
const data = pm.response.json();
|
||||
const userId = data.user.id;
|
||||
const userEmail = data.user.contact.email;
|
||||
const firstItem = data.items[0];
|
||||
|
||||
pm.environment.set("userId", userId);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('const userId = data.user.id;');
|
||||
expect(translatedCode).toContain('const userEmail = data.user.contact.email;');
|
||||
expect(translatedCode).toContain('const firstItem = data.items[0];');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userId", userId);');
|
||||
});
|
||||
|
||||
it('should handle template literals with response data', () => {
|
||||
const code = `
|
||||
const data = pm.response.json();
|
||||
const welcomeMessage = \`Hello, \${data.user.name}! Your ID is \${data.user.id}.\`;
|
||||
|
||||
pm.environment.set("welcomeMessage", welcomeMessage);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('const welcomeMessage = `Hello, ${data.user.name}! Your ID is ${data.user.id}.`;');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("welcomeMessage", welcomeMessage);');
|
||||
});
|
||||
|
||||
it('should handle response processing in arrow functions', () => {
|
||||
const code = `
|
||||
const processItems = () => {
|
||||
const items = pm.response.json().items;
|
||||
return items.map(item => item.id);
|
||||
};
|
||||
|
||||
const itemIds = processItems();
|
||||
pm.environment.set("itemIds", JSON.stringify(itemIds));
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const items = res.getBody().items;');
|
||||
expect(translatedCode).toContain('return items.map(item => item.id);');
|
||||
expect(translatedCode).toContain('const itemIds = processItems();');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("itemIds", JSON.stringify(itemIds));');
|
||||
});
|
||||
|
||||
it('should handle complex inline operations with response data', () => {
|
||||
const code = `
|
||||
const items = pm.response.json().items;
|
||||
const totalValue = items.reduce((sum, item) => sum + item.price, 0);
|
||||
const highValueItems = items.filter(item => item.price > 100);
|
||||
const itemNames = items.map(item => item.name);
|
||||
|
||||
pm.environment.set("totalValue", totalValue);
|
||||
pm.environment.set("highValueItemCount", highValueItems.length);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const items = res.getBody().items;');
|
||||
expect(translatedCode).toContain('const totalValue = items.reduce((sum, item) => sum + item.price, 0);');
|
||||
expect(translatedCode).toContain('const highValueItems = items.filter(item => item.price > 100);');
|
||||
expect(translatedCode).toContain('const itemNames = items.map(item => item.name);');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("totalValue", totalValue);');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("highValueItemCount", highValueItems.length);');
|
||||
});
|
||||
|
||||
it('should handle complex test structure with pm.response.to.have.header', () => {
|
||||
const code = `
|
||||
pm.test("Response headers validation", function() {
|
||||
pm.response.to.have.header("Content-Type", "application/json");
|
||||
pm.response.to.have.header("Cache-Control");
|
||||
|
||||
const responseTime = pm.response.responseTime;
|
||||
pm.expect(responseTime).to.be.below(1000);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check for test function conversion
|
||||
expect(translatedCode).toContain('test("Response headers validation", function() {');
|
||||
|
||||
// Check for header assertions inside the test callback
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase())');
|
||||
|
||||
// Check that other test assertions are preserved
|
||||
expect(translatedCode).toContain('const responseTime = res.getResponseTime();');
|
||||
expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);');
|
||||
});
|
||||
|
||||
it('should handle dynamic header names in pm.response.to.have.header', () => {
|
||||
const code = `
|
||||
function checkHeaderPresent(headerName) {
|
||||
pm.response.to.have.header(headerName);
|
||||
}
|
||||
|
||||
function validateHeader(headerName, expectedValue) {
|
||||
pm.response.to.have.header(headerName, expectedValue);
|
||||
}
|
||||
|
||||
checkHeaderPresent("Authorization");
|
||||
validateHeader("Content-Type", "application/json");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check function transformations
|
||||
expect(translatedCode).toContain('function checkHeaderPresent(headerName) {');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase())');
|
||||
|
||||
expect(translatedCode).toContain('function validateHeader(headerName, expectedValue) {');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);');
|
||||
|
||||
// Check function calls
|
||||
expect(translatedCode).toContain('checkHeaderPresent("Authorization");');
|
||||
expect(translatedCode).toContain('validateHeader("Content-Type", "application/json");');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Scoped Variables', () => {
|
||||
it.skip('should handle scoped variables correctly', () => {
|
||||
const code = `
|
||||
const response = pm.response;
|
||||
const status = response.status;
|
||||
|
||||
function test() {
|
||||
const response = delta.response;
|
||||
const status = response.status;
|
||||
console.log(status);
|
||||
}
|
||||
`
|
||||
const result = translateCode(code);
|
||||
console.log(result);
|
||||
expect(result).toBe(`
|
||||
const status = res.statusText;
|
||||
|
||||
function test() {
|
||||
const response = delta.response;
|
||||
const status = response.status;
|
||||
console.log(status);
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it.skip('should handle scoped variables correctly', () => {
|
||||
const code = `
|
||||
const response = delta.response;
|
||||
const status = response.status;
|
||||
|
||||
function test() {
|
||||
const response = pm.response;
|
||||
const status = response.status;
|
||||
console.log(status);
|
||||
}
|
||||
`
|
||||
const result = translateCode(code);
|
||||
console.log(result);
|
||||
expect(result).toBe(`
|
||||
const response = delta.response;
|
||||
const status = response.status;
|
||||
|
||||
function test() {
|
||||
const status = res.statusText;
|
||||
console.log(status);
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,399 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Testing Framework Translation', () => {
|
||||
// Basic testing framework translations
|
||||
it('should translate pm.test', () => {
|
||||
const code = 'pm.test("Status code is 200", function() { pm.response.to.have.status(200); });';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); });');
|
||||
});
|
||||
|
||||
it('should translate pm.expect', () => {
|
||||
const code = 'pm.expect(jsonData.success).to.be.true;';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(jsonData.success).to.be.true;');
|
||||
});
|
||||
|
||||
it('should translate pm.expect.fail', () => {
|
||||
const code = 'if (!isValid) pm.expect.fail("Data is invalid");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('if (!isValid) expect.fail("Data is invalid");');
|
||||
});
|
||||
|
||||
// Tests with response assertions
|
||||
it('should translate pm.response.to.have.status in tests', () => {
|
||||
const code = `
|
||||
pm.test("Check environment and call successful", function () {
|
||||
pm.expect(pm.environment.name).to.equal("ENVIRONMENT_NAME");
|
||||
pm.response.to.have.status(200);
|
||||
});`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
test("Check environment and call successful", function () {
|
||||
expect(bru.getEnvName()).to.equal("ENVIRONMENT_NAME");
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});`);
|
||||
});
|
||||
|
||||
// Test aliases
|
||||
it('should handle test aliases', () => {
|
||||
const code = `
|
||||
const { test, expect } = pm;
|
||||
|
||||
test("Status code is 200", function () {
|
||||
expect(pm.response.code).to.equal(200);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).not.toContain('const { test, expect } = pm');
|
||||
expect(translatedCode).toContain('test("Status code is 200", function () {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
});
|
||||
|
||||
// Tests inside different code structures
|
||||
it('should translate pm commands inside tests with nested functions', () => {
|
||||
const code = `
|
||||
pm.test("Auth flow works", function() {
|
||||
const response = pm.response.json();
|
||||
pm.expect(response.authenticated).to.be.true;
|
||||
pm.environment.set("userId", response.user.id);
|
||||
pm.collectionVariables.set("sessionId", response.session.id);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Auth flow works", function() {');
|
||||
expect(translatedCode).toContain('const response = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(response.authenticated).to.be.true;');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);');
|
||||
expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);');
|
||||
});
|
||||
|
||||
it('should translate pm.test with arrow functions', () => {
|
||||
const code = `
|
||||
pm.test("Status code is 200", () => {
|
||||
pm.expect(pm.response.code).to.eql(200);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Status code is 200", () => {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.eql(200);');
|
||||
});
|
||||
|
||||
it('should handle multiple test assertions in one function', () => {
|
||||
const code = `
|
||||
pm.test("The response has all properties", () => {
|
||||
const responseJson = pm.response.json();
|
||||
pm.expect(responseJson.type).to.eql('vip');
|
||||
pm.expect(responseJson.name).to.be.a('string');
|
||||
pm.expect(responseJson.id).to.have.lengthOf(1);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("The response has all properties", () => {');
|
||||
expect(translatedCode).toContain('const responseJson = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(responseJson.type).to.eql(\'vip\');');
|
||||
expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');');
|
||||
expect(translatedCode).toContain('expect(responseJson.id).to.have.lengthOf(1);');
|
||||
});
|
||||
|
||||
// Test with aliased variables
|
||||
it('should translate aliases within test functions', () => {
|
||||
const code = `
|
||||
const tempRes = pm.response;
|
||||
const tempTest = pm.test;
|
||||
const tempExpect = pm.expect;
|
||||
|
||||
tempTest("Status code is 200", function() {
|
||||
tempExpect(tempRes.code).to.equal(200);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).not.toContain('const tempRes = pm.response;');
|
||||
expect(translatedCode).not.toContain('const tempTest = pm.test;');
|
||||
expect(translatedCode).not.toContain('const tempExpect = pm.expect;');
|
||||
expect(translatedCode).toContain('test("Status code is 200", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
});
|
||||
|
||||
// Additional robust tests for testing framework
|
||||
it('should handle nested test functions', () => {
|
||||
const code = `
|
||||
pm.test("Main test group", function() {
|
||||
const responseJson = pm.response.json();
|
||||
|
||||
pm.test("User data validation", function() {
|
||||
pm.expect(responseJson.user).to.be.an('object');
|
||||
pm.expect(responseJson.user.id).to.be.a('string');
|
||||
});
|
||||
|
||||
pm.test("Settings validation", function() {
|
||||
pm.expect(responseJson.settings).to.be.an('object');
|
||||
pm.expect(responseJson.settings.notifications).to.be.a('boolean');
|
||||
});
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Main test group", function() {');
|
||||
expect(translatedCode).toContain('const responseJson = res.getBody();');
|
||||
expect(translatedCode).toContain('test("User data validation", function() {');
|
||||
expect(translatedCode).toContain('expect(responseJson.user).to.be.an(\'object\');');
|
||||
expect(translatedCode).toContain('test("Settings validation", function() {');
|
||||
expect(translatedCode).toContain('expect(responseJson.settings.notifications).to.be.a(\'boolean\');');
|
||||
});
|
||||
|
||||
it('should handle test with dynamic test names', () => {
|
||||
const code = `
|
||||
const endpoint = pm.variables.get("currentEndpoint");
|
||||
|
||||
pm.test(\`\${endpoint} returns correct data\`, function() {
|
||||
const responseJson = pm.response.json();
|
||||
pm.expect(responseJson).to.be.an('object');
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const endpoint = bru.getVar("currentEndpoint");');
|
||||
expect(translatedCode).toContain('test(`${endpoint} returns correct data`, function() {');
|
||||
expect(translatedCode).toContain('const responseJson = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(responseJson).to.be.an(\'object\');');
|
||||
});
|
||||
|
||||
it('should handle test with conditional execution', () => {
|
||||
const code = `
|
||||
const responseJson = pm.response.json();
|
||||
|
||||
if (responseJson.type === 'user') {
|
||||
pm.test("User validation", function() {
|
||||
pm.expect(responseJson.name).to.be.a('string');
|
||||
pm.expect(responseJson.email).to.be.a('string');
|
||||
});
|
||||
} else if (responseJson.type === 'admin') {
|
||||
pm.test("Admin validation", function() {
|
||||
pm.expect(responseJson.accessLevel).to.be.above(5);
|
||||
pm.expect(responseJson.permissions).to.be.an('array');
|
||||
});
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const responseJson = res.getBody();');
|
||||
expect(translatedCode).toContain('if (responseJson.type === \'user\') {');
|
||||
expect(translatedCode).toContain('test("User validation", function() {');
|
||||
expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');');
|
||||
expect(translatedCode).toContain('} else if (responseJson.type === \'admin\') {');
|
||||
expect(translatedCode).toContain('test("Admin validation", function() {');
|
||||
expect(translatedCode).toContain('expect(responseJson.accessLevel).to.be.above(5);');
|
||||
});
|
||||
|
||||
it('should handle assertions with logical operators', () => {
|
||||
const code = `
|
||||
pm.test("Response has valid structure", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
pm.expect(data.id && data.name).to.be.ok;
|
||||
pm.expect(data.active || data.pending).to.be.true;
|
||||
pm.expect(!data.deleted).to.be.true;
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Response has valid structure", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(data.id && data.name).to.be.ok;');
|
||||
expect(translatedCode).toContain('expect(data.active || data.pending).to.be.true;');
|
||||
expect(translatedCode).toContain('expect(!data.deleted).to.be.true;');
|
||||
});
|
||||
|
||||
it('should handle array and object assertions', () => {
|
||||
const code = `
|
||||
pm.test("Array and object validations", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
// Array validations
|
||||
pm.expect(data.items).to.be.an('array');
|
||||
pm.expect(data.items).to.have.lengthOf.at.least(1);
|
||||
pm.expect(data.items[0]).to.have.property('id');
|
||||
|
||||
// Object validations
|
||||
pm.expect(data.user).to.be.an('object');
|
||||
pm.expect(data.user).to.have.all.keys('id', 'name', 'email');
|
||||
pm.expect(data.user).to.include({active: true});
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Array and object validations", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(data.items).to.be.an(\'array\');');
|
||||
expect(translatedCode).toContain('expect(data.items).to.have.lengthOf.at.least(1);');
|
||||
expect(translatedCode).toContain('expect(data.items[0]).to.have.property(\'id\');');
|
||||
expect(translatedCode).toContain('expect(data.user).to.be.an(\'object\');');
|
||||
expect(translatedCode).toContain('expect(data.user).to.have.all.keys(\'id\', \'name\', \'email\');');
|
||||
expect(translatedCode).toContain('expect(data.user).to.include({active: true});');
|
||||
});
|
||||
|
||||
it('should handle chai assertions with deep equality', () => {
|
||||
const code = `
|
||||
pm.test("Deep equality checks", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
pm.expect(data.config).to.deep.equal({
|
||||
version: "1.0",
|
||||
active: true,
|
||||
features: ["search", "export"]
|
||||
});
|
||||
|
||||
pm.expect(data.tags).to.have.members(['api', 'test']);
|
||||
pm.expect(data.meta).to.deep.include({format: 'json'});
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Deep equality checks", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(data.config).to.deep.equal({');
|
||||
expect(translatedCode).toContain('version: "1.0",');
|
||||
expect(translatedCode).toContain('active: true,');
|
||||
expect(translatedCode).toContain('features: ["search", "export"]');
|
||||
expect(translatedCode).toContain('expect(data.tags).to.have.members([\'api\', \'test\']);');
|
||||
expect(translatedCode).toContain('expect(data.meta).to.deep.include({format: \'json\'});');
|
||||
});
|
||||
|
||||
it('should handle chai assertions with string comparisons', () => {
|
||||
const code = `
|
||||
pm.test("String validations", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
pm.expect(data.id).to.be.a('string');
|
||||
pm.expect(data.name).to.match(/^[A-Za-z\\s]+$/);
|
||||
pm.expect(data.description).to.include('API');
|
||||
pm.expect(data.url).to.have.string('api/v1');
|
||||
pm.expect(data.code).to.have.lengthOf(8);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("String validations", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(data.id).to.be.a(\'string\');');
|
||||
expect(translatedCode).toContain('expect(data.name).to.match(/^[A-Za-z\\s]+$/);');
|
||||
expect(translatedCode).toContain('expect(data.description).to.include(\'API\');');
|
||||
expect(translatedCode).toContain('expect(data.url).to.have.string(\'api/v1\');');
|
||||
expect(translatedCode).toContain('expect(data.code).to.have.lengthOf(8);');
|
||||
});
|
||||
|
||||
it('should handle assertions with numeric comparisons', () => {
|
||||
const code = `
|
||||
pm.test("Numeric validations", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
pm.expect(data.count).to.be.a('number');
|
||||
pm.expect(data.count).to.be.above(0);
|
||||
pm.expect(data.price).to.be.within(10, 100);
|
||||
pm.expect(data.discount).to.be.at.most(25);
|
||||
pm.expect(data.quantity * data.price).to.equal(data.total);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Numeric validations", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(data.count).to.be.a(\'number\');');
|
||||
expect(translatedCode).toContain('expect(data.count).to.be.above(0);');
|
||||
expect(translatedCode).toContain('expect(data.price).to.be.within(10, 100);');
|
||||
expect(translatedCode).toContain('expect(data.discount).to.be.at.most(25);');
|
||||
expect(translatedCode).toContain('expect(data.quantity * data.price).to.equal(data.total);');
|
||||
});
|
||||
|
||||
it('should handle pm.expect.fail with conditions', () => {
|
||||
const code = `
|
||||
pm.test("Validate critical fields", function() {
|
||||
const data = pm.response.json();
|
||||
|
||||
if (!data.id) {
|
||||
pm.expect.fail("Missing ID field");
|
||||
}
|
||||
|
||||
if (data.status !== 'active' && data.status !== 'pending') {
|
||||
pm.expect.fail("Invalid status: " + data.status);
|
||||
}
|
||||
|
||||
// Continue with normal assertions
|
||||
pm.expect(data.name).to.be.a('string');
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('test("Validate critical fields", function() {');
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
expect(translatedCode).toContain('if (!data.id) {');
|
||||
expect(translatedCode).toContain('expect.fail("Missing ID field");');
|
||||
expect(translatedCode).toContain('if (data.status !== \'active\' && data.status !== \'pending\') {');
|
||||
expect(translatedCode).toContain('expect.fail("Invalid status: " + data.status);');
|
||||
expect(translatedCode).toContain('expect(data.name).to.be.a(\'string\');');
|
||||
});
|
||||
|
||||
it('should handle complex test compositions', () => {
|
||||
const code = `
|
||||
// Helper function
|
||||
function validateUserObject(user) {
|
||||
pm.expect(user).to.be.an('object');
|
||||
pm.expect(user.id).to.be.a('string');
|
||||
pm.expect(user.name).to.be.a('string');
|
||||
return user.id && user.name;
|
||||
}
|
||||
|
||||
pm.test("Response validation", function() {
|
||||
const response = pm.response.json();
|
||||
const validUsers = [];
|
||||
|
||||
// Test status code
|
||||
pm.response.to.have.status(200);
|
||||
|
||||
// Test main user
|
||||
if (response.user) {
|
||||
const isValid = validateUserObject(response.user);
|
||||
if (isValid) {
|
||||
validUsers.push(response.user);
|
||||
}
|
||||
}
|
||||
|
||||
// Test related users
|
||||
if (response.relatedUsers && Array.isArray(response.relatedUsers)) {
|
||||
pm.test("Related users validation", function() {
|
||||
response.relatedUsers.forEach((user, index) => {
|
||||
pm.test(\`User at index \${index}\`, function() {
|
||||
const isValid = validateUserObject(user);
|
||||
if (isValid) {
|
||||
validUsers.push(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set the valid users for later use
|
||||
if (validUsers.length > 0) {
|
||||
pm.environment.set("validUsers", JSON.stringify(validUsers));
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Test key transformations
|
||||
expect(translatedCode).toContain('function validateUserObject(user) {');
|
||||
expect(translatedCode).toContain('expect(user).to.be.an(\'object\');');
|
||||
expect(translatedCode).toContain('test("Response validation", function() {');
|
||||
expect(translatedCode).toContain('const response = res.getBody();');
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');
|
||||
expect(translatedCode).toContain('test("Related users validation", function() {');
|
||||
expect(translatedCode).toContain('test(`User at index ${index}`, function() {');
|
||||
expect(translatedCode).toContain('bru.setEnvVar("validUsers", JSON.stringify(validUsers));');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Variable Chaining Resolution', () => {
|
||||
test('should resolve a simple variable chain (variable pointing to another variable)', () => {
|
||||
const code = `
|
||||
const original = pm.response;
|
||||
const alias = original;
|
||||
const data = alias.json();
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check that alias.json() was properly resolved to res.getBody()
|
||||
expect(translatedCode).toContain('const data = res.getBody();');
|
||||
// The original variable declarations should be removed
|
||||
expect(translatedCode).not.toContain('const original =');
|
||||
expect(translatedCode).not.toContain('const alias =');
|
||||
});
|
||||
|
||||
test('should handle mixed variable references correctly', () => {
|
||||
const code = `
|
||||
const respVar = pm.response;
|
||||
const envVar = pm.environment;
|
||||
const respAlias = respVar;
|
||||
|
||||
// These should be replaced
|
||||
const statusCode = respAlias.code;
|
||||
const envValue = envVar.get("key");
|
||||
|
||||
// This should not be replaced
|
||||
const unrelatedVar = "some value";
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Check correct replacements
|
||||
expect(translatedCode).not.toContain('const respVar');
|
||||
expect(translatedCode).not.toContain('const envVar');
|
||||
expect(translatedCode).toContain('const statusCode = res.getStatus();');
|
||||
expect(translatedCode).toContain('const envValue = bru.getEnvVar("key");');
|
||||
|
||||
// Check that unrelated variables are preserved
|
||||
expect(translatedCode).toContain('const unrelatedVar = "some value";');
|
||||
});
|
||||
|
||||
/**
|
||||
* This test verifies that when multiple variables are declared in a single statement,
|
||||
* only the ones referencing Postman objects are removed and the others are preserved.
|
||||
*
|
||||
* For example, in a statement like:
|
||||
* const response = pm.response, counter = 5, helper = "test";
|
||||
*
|
||||
* Only 'response' should be removed, resulting in:
|
||||
* const counter = 5, helper = "test";
|
||||
*/
|
||||
test('should handle multiple variables in one declaration statement', () => {
|
||||
const code = `
|
||||
// Multiple variables in one declaration, with a mix of Postman objects and regular variables
|
||||
const response = pm.response, counter = 5, helper = "test";
|
||||
|
||||
// Using both the Postman reference (should be replaced) and regular values (should be preserved)
|
||||
const statusCode = response.code;
|
||||
console.log("Counter value:", counter);
|
||||
console.log("Helper string:", helper);
|
||||
|
||||
// Another example with different Postman object
|
||||
let env = pm.environment, timeout = 1000, isValid = true;
|
||||
const baseUrl = env.get("baseUrl");
|
||||
`;
|
||||
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
// Postman references should be replaced
|
||||
expect(translatedCode).not.toContain('response = pm.response');
|
||||
expect(translatedCode).not.toContain('env = pm.environment');
|
||||
|
||||
// Regular variables should be preserved
|
||||
expect(translatedCode).toContain('const counter = 5');
|
||||
expect(translatedCode).toContain('helper = "test"');
|
||||
expect(translatedCode).toContain('timeout = 1000');
|
||||
expect(translatedCode).toContain('isValid = true');
|
||||
|
||||
// References to Postman objects should be properly translated
|
||||
expect(translatedCode).toContain('const statusCode = res.getStatus();');
|
||||
expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");');
|
||||
|
||||
// Console logs with regular variables should be preserved
|
||||
expect(translatedCode).toContain('console.log("Counter value:", counter);');
|
||||
expect(translatedCode).toContain('console.log("Helper string:", helper);');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Variables Translation', () => {
|
||||
// Regular variables tests
|
||||
it('should translate pm.variables.get', () => {
|
||||
const code = 'pm.variables.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.getVar("test");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.set', () => {
|
||||
const code = 'pm.variables.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setVar("test", "value");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.has', () => {
|
||||
const code = 'pm.variables.has("userId");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.hasVar("userId");');
|
||||
});
|
||||
|
||||
// Collection variables tests
|
||||
it('should translate pm.collectionVariables.get', () => {
|
||||
const code = 'pm.collectionVariables.get("apiUrl");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.getVar("apiUrl");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.set', () => {
|
||||
const code = 'pm.collectionVariables.set("token", jsonData.token);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setVar("token", jsonData.token);');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.has', () => {
|
||||
const code = 'pm.collectionVariables.has("authToken");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.hasVar("authToken");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.unset', () => {
|
||||
const code = 'pm.collectionVariables.unset("tempVar");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.deleteVar("tempVar");');
|
||||
});
|
||||
|
||||
// Alias tests for variables
|
||||
it('should handle variables aliases', () => {
|
||||
const code = `
|
||||
const vars = pm.variables;
|
||||
const has = vars.has("test");
|
||||
const set = vars.set("test", "value");
|
||||
const get = vars.get("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
const get = bru.getVar("test");
|
||||
`);
|
||||
});
|
||||
|
||||
// Alias tests for collection variables
|
||||
it('should handle collection variables aliases', () => {
|
||||
const code = `
|
||||
const collVars = pm.collectionVariables;
|
||||
const has = collVars.has("test");
|
||||
const set = collVars.set("test", "value");
|
||||
const get = collVars.get("test");
|
||||
const unset = collVars.unset("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
const get = bru.getVar("test");
|
||||
const unset = bru.deleteVar("test");
|
||||
`);
|
||||
});
|
||||
|
||||
// Combined tests
|
||||
it('should handle conditional expressions with variable calls', () => {
|
||||
const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";');
|
||||
});
|
||||
|
||||
it('should handle all variable methods together', () => {
|
||||
const code = `
|
||||
// All variable methods
|
||||
const hasUserId = pm.variables.has("userId");
|
||||
const userId = pm.variables.get("userId");
|
||||
pm.variables.set("requestTime", new Date().toISOString());
|
||||
|
||||
console.log(\`Has userId: \${hasUserId}, User ID: \${userId}\`);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const hasUserId = bru.hasVar("userId");');
|
||||
expect(translatedCode).toContain('const userId = bru.getVar("userId");');
|
||||
expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());');
|
||||
});
|
||||
|
||||
it('should handle all collection variable methods together', () => {
|
||||
const code = `
|
||||
// All collection variable methods
|
||||
const hasApiUrl = pm.collectionVariables.has("apiUrl");
|
||||
const apiUrl = pm.collectionVariables.get("apiUrl");
|
||||
pm.collectionVariables.set("requestTime", new Date().toISOString());
|
||||
pm.collectionVariables.unset("tempVar");
|
||||
|
||||
console.log(\`Has API URL: \${hasApiUrl}, API URL: \${apiUrl}\`);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toContain('const hasApiUrl = bru.hasVar("apiUrl");');
|
||||
expect(translatedCode).toContain('const apiUrl = bru.getVar("apiUrl");');
|
||||
expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());');
|
||||
expect(translatedCode).toContain('bru.deleteVar("tempVar");');
|
||||
});
|
||||
|
||||
it('should handle more complex nested expressions with variables', () => {
|
||||
const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { getMemberExpressionString } from '../../src/utils/jscode-shift-translator';
|
||||
const j = require('jscodeshift');
|
||||
|
||||
describe('getMemberExpressionString', () => {
|
||||
it('should correctly convert simple member expressions to strings', () => {
|
||||
// Create a simple member expression: pm.environment.get
|
||||
const memberExpr = j.memberExpression(
|
||||
j.memberExpression(
|
||||
j.identifier('pm'),
|
||||
j.identifier('environment')
|
||||
),
|
||||
j.identifier('get')
|
||||
);
|
||||
|
||||
const result = getMemberExpressionString(memberExpr);
|
||||
expect(result).toBe('pm.environment.get');
|
||||
});
|
||||
|
||||
it('should handle computed properties with string literals', () => {
|
||||
// Create a computed member expression: pm["environment"]["get"]
|
||||
const memberExpr = j.memberExpression(
|
||||
j.memberExpression(
|
||||
j.identifier('pm'),
|
||||
j.literal('environment'),
|
||||
true // computed
|
||||
),
|
||||
j.literal('get'),
|
||||
true // computed
|
||||
);
|
||||
|
||||
const result = getMemberExpressionString(memberExpr);
|
||||
expect(result).toBe('pm.environment.get');
|
||||
});
|
||||
|
||||
it('should mark non-string computed properties as [computed]', () => {
|
||||
// Create a computed member expression with variable: obj[varName]
|
||||
const memberExpr = j.memberExpression(
|
||||
j.identifier('obj'),
|
||||
j.identifier('varName'),
|
||||
true // computed
|
||||
);
|
||||
|
||||
const result = getMemberExpressionString(memberExpr);
|
||||
expect(result).toBe('obj.[computed]');
|
||||
});
|
||||
|
||||
it('should handle basic identifiers', () => {
|
||||
const identifier = j.identifier('pm');
|
||||
const result = getMemberExpressionString(identifier);
|
||||
expect(result).toBe('pm');
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/node-machine-id": "^2.0.0",
|
||||
|
||||
@@ -220,7 +220,6 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
}
|
||||
}
|
||||
|
||||
// Is this a folder.bru file?
|
||||
if (path.basename(pathname) === 'folder.bru') {
|
||||
const file = {
|
||||
meta: {
|
||||
@@ -327,16 +326,26 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
|
||||
let name = path.basename(pathname);
|
||||
let seq = 1;
|
||||
const folderBruFilePath = path.join(pathname, `folder.bru`);
|
||||
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruData = await collectionBruToJson(folderBruFileContent);
|
||||
name = folderBruData?.meta?.name || name;
|
||||
seq = folderBruData?.meta?.seq || seq;
|
||||
}
|
||||
|
||||
const directory = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name
|
||||
name,
|
||||
seq,
|
||||
uid: getRequestUid(pathname)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ const collectionBruToJson = async (data, parsed = false) => {
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
if (json.meta) {
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
transformedJson.meta = {
|
||||
name: json.meta.name
|
||||
name: json.meta.name,
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,9 +63,11 @@ const jsonToCollectionBru = async (json, isFolder) => {
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
collectionBruJson.meta = {
|
||||
name: json.meta.name
|
||||
name: json.meta.name,
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const fsPromises = require('fs/promises');
|
||||
const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { ipcMain, shell, dialog, app } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
|
||||
const brunoConverters = require('@usebruno/converters');
|
||||
const { postmanToBruno } = brunoConverters;
|
||||
|
||||
const {
|
||||
writeFile,
|
||||
@@ -22,7 +25,10 @@ const {
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB,
|
||||
safeWriteFileSync
|
||||
safeWriteFileSync,
|
||||
copyPath,
|
||||
removePath,
|
||||
getPaths
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@@ -32,11 +38,10 @@ const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const CollectionSecurityStore = require('../store/collection-security');
|
||||
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
|
||||
const interpolateVars = require('./network/interpolate-vars');
|
||||
const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection');
|
||||
const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection');
|
||||
const { getProcessEnvVars } = require('../store/process-env');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2');
|
||||
const { getCertsAndProxyConfig } = require('./network');
|
||||
const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
const collectionSecurityStore = new CollectionSecurityStore();
|
||||
@@ -192,12 +197,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:save-folder-root', async (event, folder) => {
|
||||
try {
|
||||
const { name: folderName, root: folderRoot, pathname: folderPathname } = folder;
|
||||
const { name: folderName, root: folderRoot = {}, pathname: folderPathname } = folder;
|
||||
const folderBruFilePath = path.join(folderPathname, 'folder.bru');
|
||||
|
||||
folderRoot.meta = {
|
||||
name: folderName
|
||||
};
|
||||
if (!folderRoot.meta) {
|
||||
folderRoot.meta = {
|
||||
name: folderName,
|
||||
seq: 1
|
||||
};
|
||||
}
|
||||
|
||||
const content = await jsonToCollectionBru(
|
||||
folderRoot,
|
||||
@@ -376,14 +384,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent.meta.name = newName;
|
||||
} else {
|
||||
folderBruFileJsonContent = {};
|
||||
folderBruFileJsonContent = {
|
||||
meta: {
|
||||
name: newName,
|
||||
seq: 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
folderBruFileJsonContent.meta = {
|
||||
name: newName,
|
||||
};
|
||||
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
@@ -425,14 +435,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent.meta.name = newName;
|
||||
} else {
|
||||
folderBruFileJsonContent = {};
|
||||
folderBruFileJsonContent = {
|
||||
meta: {
|
||||
name: newName,
|
||||
seq: 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
folderBruFileJsonContent.meta = {
|
||||
name: newName,
|
||||
};
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
@@ -512,6 +524,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
let data = {
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: 1
|
||||
}
|
||||
};
|
||||
const content = await jsonToCollectionBru(data, true); // isFolder flag
|
||||
@@ -598,6 +611,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
if (item?.root?.meta?.name) {
|
||||
const folderBruFilePath = path.join(folderPath, 'folder.bru');
|
||||
item.root.meta.seq = item.seq;
|
||||
const folderContent = await jsonToCollectionBru(
|
||||
item.root,
|
||||
true // isFolder
|
||||
@@ -731,17 +745,42 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
|
||||
try {
|
||||
for await (let item of itemsToResequence) {
|
||||
const bru = fs.readFileSync(item.pathname, 'utf8');
|
||||
const jsonData = await bruToJsonViaWorker(bru);
|
||||
|
||||
if (jsonData.seq !== item.seq) {
|
||||
jsonData.seq = item.seq;
|
||||
const content = await jsonToBruViaWorker(jsonData);
|
||||
await writeFile(item.pathname, content);
|
||||
for (let item of itemsToResequence) {
|
||||
if (item?.type === 'folder') {
|
||||
const folderRootPath = path.join(item.pathname, 'folder.bru');
|
||||
let folderBruJsonData = {
|
||||
meta: {
|
||||
name: path.basename(item?.pathname),
|
||||
seq: item?.seq || 1
|
||||
}
|
||||
};
|
||||
if (fs.existsSync(folderRootPath)) {
|
||||
const bru = fs.readFileSync(folderRootPath, 'utf8');
|
||||
folderBruJsonData = await collectionBruToJson(bru);
|
||||
if (!folderBruJsonData?.meta) {
|
||||
folderBruJsonData.meta = {
|
||||
name: path.basename(item?.pathname),
|
||||
seq: item?.seq || 1
|
||||
};
|
||||
}
|
||||
if (folderBruJsonData?.meta?.seq === item.seq) {
|
||||
continue;
|
||||
}
|
||||
folderBruJsonData.meta.seq = item.seq;
|
||||
}
|
||||
const content = await jsonToCollectionBru(folderBruJsonData);
|
||||
await writeFile(folderRootPath, content);
|
||||
} else {
|
||||
if (fs.existsSync(item.pathname)) {
|
||||
const itemToSave = transformRequestToSaveToFilesystem(item);
|
||||
const content = await jsonToBruViaWorker(itemToSave);
|
||||
await writeFile(item.pathname, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error in resequence-items:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
@@ -760,6 +799,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => {
|
||||
try {
|
||||
if (fs.existsSync(targetDirname)) {
|
||||
const sourceDirname = path.dirname(sourcePathname);
|
||||
const pathnamesBefore = await getPaths(sourcePathname);
|
||||
const pathnamesAfter = pathnamesBefore?.map(p => p?.replace(sourceDirname, targetDirname));
|
||||
await copyPath(sourcePathname, targetDirname);
|
||||
await removePath(sourcePathname);
|
||||
// move the request uids of the previous file/folders to the new file/folder items
|
||||
pathnamesAfter?.forEach((_, index) => {
|
||||
moveRequestUid(pathnamesBefore[index], pathnamesAfter[index]);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => {
|
||||
try {
|
||||
const folderName = path.basename(folderPath);
|
||||
@@ -1096,6 +1153,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Implement the Postman to Bruno conversion handler
|
||||
ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => {
|
||||
try {
|
||||
// Convert Postman collection to Bruno format
|
||||
const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true});
|
||||
|
||||
return brunoCollection;
|
||||
} catch (error) {
|
||||
console.error('Error converting Postman to Bruno:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
@@ -1255,6 +1255,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
folderUid
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { get, each, find, compact, filter } = require('lodash');
|
||||
const { get, each, find, compact, isString, filter } = require('lodash');
|
||||
const fs = require('fs');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { uuid } = require('./common');
|
||||
@@ -205,6 +205,14 @@ const findParentItemInCollection = (collection, itemUid) => {
|
||||
});
|
||||
};
|
||||
|
||||
const findParentItemInCollectionByPathname = (collection, pathname) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return find(flattenedItems, (item) => {
|
||||
return item.items && find(item.items, (i) => i.pathname === pathname);
|
||||
});
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item.uid);
|
||||
@@ -272,12 +280,73 @@ const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
return findItemByPathname(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return str.replaceAll('\t', ' '.repeat(numSpaces));
|
||||
};
|
||||
|
||||
const transformRequestToSaveToFilesystem = (item) => {
|
||||
const _item = item.draft ? item.draft : item;
|
||||
const itemToSave = {
|
||||
uid: _item.uid,
|
||||
type: _item.type,
|
||||
name: _item.name,
|
||||
seq: _item.seq,
|
||||
request: {
|
||||
method: _item.request.method,
|
||||
url: _item.request.url,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: _item.request.auth,
|
||||
body: _item.request.body,
|
||||
script: _item.request.script,
|
||||
vars: _item.request.vars,
|
||||
assertions: _item.request.assertions,
|
||||
tests: _item.request.tests,
|
||||
docs: _item.request.docs
|
||||
}
|
||||
};
|
||||
|
||||
each(_item.request.params, (param) => {
|
||||
itemToSave.request.params.push({
|
||||
uid: param.uid,
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: param.type,
|
||||
enabled: param.enabled
|
||||
});
|
||||
});
|
||||
|
||||
each(_item.request.headers, (header) => {
|
||||
itemToSave.request.headers.push({
|
||||
uid: header.uid,
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: header.enabled
|
||||
});
|
||||
});
|
||||
|
||||
if (itemToSave.request.body.mode === 'json') {
|
||||
itemToSave.request.body = {
|
||||
...itemToSave.request.body,
|
||||
json: replaceTabsWithSpaces(itemToSave.request.body.json)
|
||||
};
|
||||
}
|
||||
|
||||
return itemToSave;
|
||||
}
|
||||
|
||||
const sortCollection = (collection) => {
|
||||
const items = collection.items || [];
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
folderItems = folderItems.sort((a, b) => a.seq - b.seq);
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
collection.items = folderItems.concat(requestItems);
|
||||
@@ -292,7 +361,7 @@ const sortFolder = (folder = {}) => {
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
folderItems = folderItems.sort((a, b) => a.seq - b.seq);
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
folder.items = folderItems.concat(requestItems);
|
||||
@@ -410,11 +479,13 @@ module.exports = {
|
||||
findItemByPathname,
|
||||
findItemInCollectionByPathname,
|
||||
findParentItemInCollection,
|
||||
findParentItemInCollectionByPathname,
|
||||
parseBruFileMeta,
|
||||
hydrateRequestWithUuid,
|
||||
transformRequestToSaveToFilesystem,
|
||||
sortCollection,
|
||||
sortFolder,
|
||||
getAllRequestsInFolderRecursively,
|
||||
getEnvVars,
|
||||
getFormattedCollectionOauth2Credentials,
|
||||
hydrateRequestWithUuid
|
||||
getFormattedCollectionOauth2Credentials
|
||||
};
|
||||
@@ -164,9 +164,9 @@ const searchForBruFiles = (dir) => {
|
||||
const sanitizeName = (name) => {
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g;
|
||||
name = name
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[.\s]+/, '') // remove leading dots and and spaces
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[\s\-]+/, '') // remove leading spaces and hyphens
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces
|
||||
return name;
|
||||
};
|
||||
|
||||
@@ -175,10 +175,11 @@ const isWindowsOS = () => {
|
||||
}
|
||||
|
||||
const validateName = (name) => {
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // keeping this for informational purpose
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
|
||||
const firstCharacter = /^[^\s\-<>:"/\\|?*\x00-\x1F]/; // no space, hyphen and `invalidCharacters`
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no `invalidCharacters`
|
||||
const lastCharacter = /[^.\s<>:"/\\|?*\x00-\x1F]$/; // no dot, space and `invalidCharacters`
|
||||
if (name.length > 255) return false; // max name length
|
||||
|
||||
if (reservedDeviceNames.test(name)) return false; // windows reserved names
|
||||
@@ -282,6 +283,67 @@ function safeWriteFileSync(filePath, data) {
|
||||
fs.writeFileSync(safePath, data);
|
||||
}
|
||||
|
||||
// Recursively copies a source <file/directory> to a destination <directory>.
|
||||
const copyPath = async (source, destination) => {
|
||||
let targetPath = `${destination}/${path.basename(source)}`;
|
||||
|
||||
const targetPathExists = await fsPromises.access(targetPath).then(() => true).catch(() => false);
|
||||
if (targetPathExists) {
|
||||
throw new Error(`Cannot copy, ${path.basename(source)} already exists in ${path.basename(destination)}`);
|
||||
}
|
||||
|
||||
const copy = async (source, destination) => {
|
||||
const stat = await fsPromises.lstat(source);
|
||||
if (stat.isDirectory()) {
|
||||
await fsPromises.mkdir(destination, { recursive: true });
|
||||
const entries = await fsPromises.readdir(source);
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(source, entry);
|
||||
const destPath = path.join(destination, entry);
|
||||
await copy(srcPath, destPath);
|
||||
}
|
||||
} else {
|
||||
await fsPromises.copyFile(source, destination);
|
||||
}
|
||||
}
|
||||
|
||||
await copy(source, targetPath);
|
||||
}
|
||||
|
||||
// Recursively removes a source <file/directory>.
|
||||
const removePath = async (source) => {
|
||||
const stat = await fsPromises.lstat(source);
|
||||
if (stat.isDirectory()) {
|
||||
const entries = await fsPromises.readdir(source);
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(source, entry);
|
||||
await removePath(entryPath);
|
||||
}
|
||||
await fsPromises.rmdir(source);
|
||||
} else {
|
||||
await fsPromises.unlink(source);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively gets paths.
|
||||
const getPaths = async (source) => {
|
||||
let paths = [];
|
||||
const _getPaths = async (source) => {
|
||||
const stat = await fsPromises.lstat(source);
|
||||
paths.push(source);
|
||||
if (stat.isDirectory()) {
|
||||
const entries = await fsPromises.readdir(source);
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(source, entry);
|
||||
await _getPaths(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
await _getPaths(source);
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
isValidPathname,
|
||||
exists,
|
||||
@@ -308,5 +370,8 @@ module.exports = {
|
||||
getCollectionStats,
|
||||
sizeInMB,
|
||||
safeWriteFile,
|
||||
safeWriteFileSync
|
||||
safeWriteFileSync,
|
||||
copyPath,
|
||||
removePath,
|
||||
getPaths
|
||||
};
|
||||
|
||||
@@ -2,9 +2,13 @@ const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = r
|
||||
|
||||
describe('sanitizeName', () => {
|
||||
it('should replace invalid characters with hyphens', () => {
|
||||
const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F';
|
||||
const expectedOutput = '----------------------------------------';
|
||||
expect(sanitizeName(input)).toEqual(expectedOutput);
|
||||
expect(sanitizeName(
|
||||
'valid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'
|
||||
)).toEqual('valid----------------------------------------');
|
||||
|
||||
expect(sanitizeName(
|
||||
'<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1Fvalid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'
|
||||
)).toEqual('valid----------------------------------------');
|
||||
});
|
||||
|
||||
it('should not modify valid directory names', () => {
|
||||
|
||||
116
packages/bruno-electron/src/utils/tests/filesystem/index.spec.js
Normal file
116
packages/bruno-electron/src/utils/tests/filesystem/index.spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs/promises');
|
||||
const os = require('os');
|
||||
const { copyPath, removePath } = require('../../filesystem');
|
||||
const { initialCollectionStructure, finalCollectionStructure } = require('../fixtures/filesystem/copypath-removepath');
|
||||
|
||||
describe('File System Operations', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a temporary directory for each test
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bruno-test-'));
|
||||
await createFilesAndFolders(tempDir, initialCollectionStructure);
|
||||
const result = await verifyFilesAndFolders(tempDir, initialCollectionStructure);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// clean up after each test
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
// confirm the temp directory is deleted
|
||||
expect(await fs.access(tempDir).then(() => true).catch(() => false)).toBe(false);
|
||||
});
|
||||
|
||||
describe('copyPath and removePath', () => {
|
||||
it('should move files and folder items multiple times', async () => {
|
||||
|
||||
{
|
||||
const sourcePath = path.join(tempDir, 'folder_1', 'file_2.bru');
|
||||
const destDir = path.join(tempDir, 'folder_1', 'folder_1_1');
|
||||
await copyPath(sourcePath, destDir);
|
||||
await removePath(sourcePath);
|
||||
}
|
||||
|
||||
{
|
||||
const sourcePath = path.join(tempDir, 'folder_2');
|
||||
const destDir = path.join(tempDir, 'folder_1', 'folder_1_1');
|
||||
await copyPath(sourcePath, destDir);
|
||||
await removePath(sourcePath);
|
||||
}
|
||||
|
||||
{
|
||||
const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'file_2_2.bru');
|
||||
const destDir = path.join(tempDir, 'folder_1');
|
||||
await copyPath(sourcePath, destDir);
|
||||
await removePath(sourcePath);
|
||||
}
|
||||
|
||||
{
|
||||
const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'folder_2_1');
|
||||
const destDir = path.join(tempDir);
|
||||
await copyPath(sourcePath, destDir);
|
||||
await removePath(sourcePath);
|
||||
}
|
||||
|
||||
const result = await verifyFilesAndFolders(tempDir, finalCollectionStructure);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should throw an error move file/folder if the destination has the same filename', async () => {
|
||||
{
|
||||
const sourcePath = path.join(tempDir, 'folder_1', 'file_dup.bru');
|
||||
const destDir = path.join(tempDir, 'folder_1');
|
||||
await expect(copyPath(sourcePath, destDir)).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// create folders and files recursively based on the defined json structure
|
||||
const createFilesAndFolders = async (dir, filesAndFolders) => {
|
||||
for (const item of filesAndFolders) {
|
||||
const itemPath = path.join(dir, item.name);
|
||||
if (item.type === 'folder') {
|
||||
await fs.mkdir(itemPath, { recursive: true });
|
||||
await createFilesAndFolders(itemPath, item.files);
|
||||
} else {
|
||||
await fs.writeFile(itemPath, item.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if a file/folder doesnt exist, return false
|
||||
// should only contain files and folders that are defined in the json structure
|
||||
const verifyFilesAndFolders = async (dir, filesAndFolders) => {
|
||||
const verify = async (dir, filesAndFolders) => {
|
||||
const files = await fs.readdir(dir);
|
||||
if (files.length !== filesAndFolders.length) {
|
||||
return false;
|
||||
}
|
||||
for (const file of files) {
|
||||
const itemPath = path.join(dir, file);
|
||||
const item = filesAndFolders.find(f => f.name === file);
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (item.type === 'folder') {
|
||||
return await verify(itemPath, item.files);
|
||||
} else {
|
||||
return await fs.readFile(itemPath, 'utf8').then(content => content === item.content);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const verified = await verify(dir, filesAndFolders);
|
||||
return verified;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
155
packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js
vendored
Normal file
155
packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
const initialCollectionStructure = [
|
||||
{
|
||||
"name": "folder_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_content"
|
||||
},
|
||||
{
|
||||
"name": "file_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_content"
|
||||
},
|
||||
{
|
||||
"name": "folder_1_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_1_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_1_content"
|
||||
},
|
||||
{
|
||||
"name": "file_1_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_2_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_1_3.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_3_content"
|
||||
},
|
||||
{
|
||||
"name": "file_dup.bru",
|
||||
"type": "file",
|
||||
"content": "file_dup_content"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "folder_2",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_2_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_1_content"
|
||||
},
|
||||
{
|
||||
"name": "file_2_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_2_content"
|
||||
},
|
||||
{
|
||||
"name": "folder_2_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_2_1_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_1_1_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_dup.bru",
|
||||
"type": "file",
|
||||
"content": "file_dup_content"
|
||||
}
|
||||
];
|
||||
|
||||
const finalCollectionStructure = [
|
||||
{
|
||||
"name": "folder_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_content"
|
||||
},
|
||||
{
|
||||
"name": "folder_1_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_1_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_1_content"
|
||||
},
|
||||
{
|
||||
"name": "file_1_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_2_content"
|
||||
},
|
||||
{
|
||||
"name": "file_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_content"
|
||||
},
|
||||
{
|
||||
"name": "folder_2",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_2_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_1_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_1_3.bru",
|
||||
"type": "file",
|
||||
"content": "file_1_3_content"
|
||||
},
|
||||
{
|
||||
"name": "file_2_2.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_2_content"
|
||||
},
|
||||
{
|
||||
"name": "file_dup.bru",
|
||||
"type": "file",
|
||||
"content": "file_dup_content"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "folder_2_1",
|
||||
"type": "folder",
|
||||
"files": [
|
||||
{
|
||||
"name": "file_2_1_1.bru",
|
||||
"type": "file",
|
||||
"content": "file_2_1_1_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_dup.bru",
|
||||
"type": "file",
|
||||
"content": "file_dup_content"
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { initialCollectionStructure, finalCollectionStructure };
|
||||
@@ -35,6 +35,7 @@
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"tv4": "^1.3.0",
|
||||
"uuid": "^9.0.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user