mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 00:24:08 +00:00
feat: add path based grouping for openapi (#5638)
* feat: add path based grouping for openapi
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
const FullscreenLoader = ({ isLoading }) => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullscreenLoader;
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { IconLoader2, IconFileImport } from '@tabler/icons';
|
||||
import { IconFileImport } from '@tabler/icons';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isOpenApiSpec, convertOpenapiToBruno } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import ImportSettings from 'components/Sidebar/ImportSettings';
|
||||
import FullscreenLoader from './FullscreenLoader/index';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
const text = await file.text();
|
||||
@@ -26,60 +28,22 @@ const convertFileToObject = async (file) => {
|
||||
}
|
||||
};
|
||||
|
||||
const FullscreenLoader = ({ isLoading }) => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [showImportSettings, setShowImportSettings] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState(null);
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleDrag = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
@@ -87,30 +51,43 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSettings = () => {
|
||||
try {
|
||||
const collection = convertOpenapiToBruno(openApiData, { groupBy: groupingType });
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastError(err, 'Failed to process OpenAPI specification');
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await convertFileToObject(file);
|
||||
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Failed to parse file content');
|
||||
}
|
||||
|
||||
|
||||
// Check if it's an OpenAPI spec and show settings
|
||||
if (isOpenApiSpec(data)) {
|
||||
setOpenApiData(data);
|
||||
setIsLoading(false);
|
||||
setShowImportSettings(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let collection;
|
||||
|
||||
|
||||
if (isPostmanCollection(data)) {
|
||||
collection = await postmanToBruno(data);
|
||||
}
|
||||
else if (isInsomniaCollection(data)) {
|
||||
} else if (isInsomniaCollection(data)) {
|
||||
collection = convertInsomniaToBruno(data);
|
||||
}
|
||||
else if (isOpenApiSpec(data)) {
|
||||
collection = convertOpenapiToBruno(data);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
collection = await processBrunoCollection(data);
|
||||
}
|
||||
|
||||
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
toastError(err, 'Import collection failed');
|
||||
@@ -123,7 +100,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
await processFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
@@ -143,19 +120,23 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
return <FullscreenLoader isLoading={isLoading} />;
|
||||
}
|
||||
|
||||
const acceptedFileTypes = [
|
||||
'.json',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'application/json',
|
||||
'application/yaml',
|
||||
'application/x-yaml'
|
||||
]
|
||||
const acceptedFileTypes = ['.json', '.yaml', '.yml', 'application/json', 'application/yaml', 'application/x-yaml'];
|
||||
|
||||
if (showImportSettings) {
|
||||
return (
|
||||
<ImportSettings
|
||||
groupingType={groupingType}
|
||||
setGroupingType={setGroupingType}
|
||||
onClose={onClose}
|
||||
onConfirm={handleImportSettings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose} dataTestId="import-collection-modal">
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Import from file</h3>
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
@@ -164,16 +145,13 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-6 transition-colors duration-200
|
||||
${dragActive
|
||||
? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
}
|
||||
${dragActive ? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700'}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<IconFileImport
|
||||
size={28}
|
||||
className="text-gray-400 dark:text-gray-500 mb-3"
|
||||
<IconFileImport
|
||||
size={28}
|
||||
className="text-gray-400 dark:text-gray-500 mb-3"
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-group {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.badge.border || 'transparent'};
|
||||
}
|
||||
|
||||
.current-group:hover {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.hoverBg || props.theme.sidebar.badge.bg};
|
||||
}
|
||||
|
||||
/* Fix dropdown positioning */
|
||||
[data-tippy-root] {
|
||||
left: 0 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportSettings = ({
|
||||
groupingType,
|
||||
setGroupingType,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex items-center justify-between w-full current-group"
|
||||
data-testid="grouping-dropdown"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="OpenAPI Import Settings"
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onConfirm}
|
||||
confirmText="Import"
|
||||
dataTestId="import-settings-modal"
|
||||
>
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Folder arrangement</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportSettings;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { openApiToBruno } from '@usebruno/converters';
|
||||
|
||||
export const convertOpenapiToBruno = (data) => {
|
||||
export const convertOpenapiToBruno = (data, options = {}) => {
|
||||
try {
|
||||
return openApiToBruno(data);
|
||||
return openApiToBruno(data, options);
|
||||
} catch (err) {
|
||||
console.error('Error converting OpenAPI to Bruno:', err);
|
||||
throw new BrunoError('Import collection failed: ' + err.message);
|
||||
|
||||
@@ -45,12 +45,21 @@ const builder = (yargs) => {
|
||||
describe: 'Skip SSL certificate verification when fetching from URLs',
|
||||
default: false
|
||||
})
|
||||
.option('group-by', {
|
||||
alias: 'g',
|
||||
describe: 'How to group the imported requests: "tags" groups by OpenAPI tags, "path" groups by URL path structure',
|
||||
type: 'string',
|
||||
choices: ['tags', 'path'],
|
||||
default: 'tags'
|
||||
})
|
||||
.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"');
|
||||
.example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"')
|
||||
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --group-by path')
|
||||
.example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -g tags');
|
||||
};
|
||||
|
||||
const isUrl = (str) => {
|
||||
@@ -132,7 +141,7 @@ const readOpenApiFile = async (source, options = {}) => {
|
||||
|
||||
const handler = async (argv) => {
|
||||
try {
|
||||
const { type, source, output, outputFile, collectionName, insecure } = argv;
|
||||
const { type, source, output, outputFile, collectionName, insecure, groupBy } = argv;
|
||||
|
||||
if (!type || type !== 'openapi') {
|
||||
console.error(chalk.red('Only OpenAPI import is supported currently'));
|
||||
@@ -161,7 +170,7 @@ const handler = async (argv) => {
|
||||
console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
|
||||
|
||||
// Convert OpenAPI to Bruno format
|
||||
let brunoCollection = openApiToBruno(openApiSpec);
|
||||
let brunoCollection = openApiToBruno(openApiSpec, { groupBy });
|
||||
|
||||
// Override collection name if provided
|
||||
if (collectionName) {
|
||||
|
||||
@@ -394,6 +394,95 @@ const groupRequestsByTags = (requests) => {
|
||||
return [groups, ungrouped];
|
||||
};
|
||||
|
||||
const groupRequestsByPath = (requests) => {
|
||||
const pathGroups = {};
|
||||
|
||||
// Group requests by their path segments
|
||||
requests.forEach((request) => {
|
||||
// Use original path for grouping to preserve {id} format
|
||||
const pathToUse = request.originalPath || request.path;
|
||||
const pathSegments = pathToUse.split('/').filter((segment) => segment !== '');
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
// Handle root path or paths with only parameters
|
||||
const groupName = 'Root';
|
||||
if (!pathGroups[groupName]) {
|
||||
pathGroups[groupName] = {
|
||||
name: groupName,
|
||||
requests: [],
|
||||
subGroups: {}
|
||||
};
|
||||
}
|
||||
pathGroups[groupName].requests.push(request);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the first segment as the main group
|
||||
let groupName = pathSegments[0];
|
||||
|
||||
if (!pathGroups[groupName]) {
|
||||
pathGroups[groupName] = {
|
||||
name: groupName,
|
||||
requests: [],
|
||||
subGroups: {}
|
||||
};
|
||||
}
|
||||
|
||||
// If there's only one meaningful segment, add to main group
|
||||
if (pathSegments.length <= 1) {
|
||||
pathGroups[groupName].requests.push(request);
|
||||
} else {
|
||||
// For deeper paths, create sub-groups
|
||||
let currentGroup = pathGroups[groupName];
|
||||
for (let i = 1; i < pathSegments.length; i++) {
|
||||
let subGroupName = pathSegments[i];
|
||||
|
||||
if (!currentGroup.subGroups[subGroupName]) {
|
||||
currentGroup.subGroups[subGroupName] = {
|
||||
name: subGroupName,
|
||||
requests: [],
|
||||
subGroups: {}
|
||||
};
|
||||
}
|
||||
currentGroup = currentGroup.subGroups[subGroupName];
|
||||
}
|
||||
currentGroup.requests.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert the nested structure to Bruno folder format
|
||||
const buildFolderStructure = (group) => {
|
||||
// Create a new usedNames set for each folder/subfolder scope
|
||||
const localUsedNames = new Set();
|
||||
const items = group.requests.map((req) => transformOpenapiRequestItem(req, localUsedNames));
|
||||
|
||||
// Add sub-folders
|
||||
const subFolders = [];
|
||||
Object.values(group.subGroups).forEach((subGroup) => {
|
||||
const subFolderItems = buildFolderStructure(subGroup);
|
||||
if (subFolderItems.length > 0) {
|
||||
subFolders.push({
|
||||
uid: uuid(),
|
||||
name: subGroup.name,
|
||||
type: 'folder',
|
||||
items: subFolderItems
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...items, ...subFolders];
|
||||
};
|
||||
|
||||
const folders = Object.values(pathGroups).map((group) => ({
|
||||
uid: uuid(),
|
||||
name: group.name,
|
||||
type: 'folder',
|
||||
items: buildFolderStructure(group)
|
||||
}));
|
||||
|
||||
return folders;
|
||||
};
|
||||
|
||||
const getDefaultUrl = (serverObject) => {
|
||||
let url = serverObject.url;
|
||||
if (serverObject.variables) {
|
||||
@@ -439,9 +528,8 @@ const openAPIRuntimeExpressionToScript = (expression) => {
|
||||
return expression;
|
||||
};
|
||||
|
||||
export const parseOpenApiCollection = (data) => {
|
||||
export const parseOpenApiCollection = (data, options = {}) => {
|
||||
const usedNames = new Set();
|
||||
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
@@ -504,9 +592,10 @@ export const parseOpenApiCollection = (data) => {
|
||||
return {
|
||||
method: method,
|
||||
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
|
||||
originalPath: path, // Keep original path for grouping
|
||||
operationObject: operationObject,
|
||||
global: {
|
||||
server: '{{baseUrl}}',
|
||||
server: '{{baseUrl}}',
|
||||
security: securityConfig
|
||||
}
|
||||
};
|
||||
@@ -514,6 +603,13 @@ export const parseOpenApiCollection = (data) => {
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []); // flatten
|
||||
|
||||
// Support both tag-based and path-based grouping
|
||||
const groupingType = options.groupBy || 'tags';
|
||||
|
||||
if (groupingType === 'path') {
|
||||
brunoCollection.items = groupRequestsByPath(allRequests);
|
||||
} else {
|
||||
// Default tag-based grouping
|
||||
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
|
||||
let brunoFolders = groups.map((group) => {
|
||||
return {
|
||||
@@ -535,13 +631,14 @@ export const parseOpenApiCollection = (data) => {
|
||||
name: group.name
|
||||
}
|
||||
},
|
||||
items: group.requests.map(req => transformOpenapiRequestItem(req, usedNames)),
|
||||
items: group.requests.map((req) => transformOpenapiRequestItem(req, usedNames))
|
||||
};
|
||||
});
|
||||
|
||||
let ungroupedItems = ungroupedRequests.map(req => transformOpenapiRequestItem(req, usedNames));
|
||||
let ungroupedItems = ungroupedRequests.map((req) => transformOpenapiRequestItem(req, usedNames));
|
||||
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
|
||||
brunoCollection.items = brunoCollectionItems;
|
||||
}
|
||||
|
||||
// Determine collection-level authentication based on global security requirements
|
||||
const buildCollectionAuth = (scheme) => {
|
||||
@@ -648,13 +745,13 @@ export const parseOpenApiCollection = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const openApiToBruno = (openApiSpecification) => {
|
||||
export const openApiToBruno = (openApiSpecification, options = {}) => {
|
||||
try {
|
||||
if(typeof openApiSpecification !== 'object') {
|
||||
openApiSpecification = jsyaml.load(openApiSpecification);
|
||||
}
|
||||
|
||||
const collection = parseOpenApiCollection(openApiSpecification);
|
||||
const collection = parseOpenApiCollection(openApiSpecification, options);
|
||||
const transformedCollection = transformItemsInCollection(collection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
const validatedCollection = validateSchema(hydratedCollection);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
const openApiSpec = {
|
||||
openapi: '3.0.0',
|
||||
info: { title: 'Parameter API', version: '1.0.0' },
|
||||
servers: [{ url: 'https://api.example.com' }],
|
||||
paths: {
|
||||
'/{id}': {
|
||||
get: {
|
||||
summary: 'Get by ID',
|
||||
operationId: 'getById',
|
||||
responses: { 200: { description: 'OK' } }
|
||||
}
|
||||
},
|
||||
'/{id}/{subId}': {
|
||||
get: {
|
||||
summary: 'Get by ID and sub ID',
|
||||
operationId: 'getByIdAndSubId',
|
||||
responses: { 200: { description: 'OK' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('openapi-import-grouping', () => {
|
||||
it('should handle path based grouping', () => {
|
||||
const result = openApiToBruno(openApiSpec, { groupBy: 'path' });
|
||||
|
||||
// Should have one folder containing both requests
|
||||
expect(result.items).toHaveLength(1);
|
||||
|
||||
const folder = result.items[0];
|
||||
expect(folder.name).toBe('{id}');
|
||||
expect(folder.type).toBe('folder');
|
||||
|
||||
// Folder should contain one request and one subfolder
|
||||
expect(folder.items).toHaveLength(2);
|
||||
|
||||
const requests = folder.items.filter((item) => item.type === 'http-request');
|
||||
const subfolders = folder.items.filter((item) => item.type === 'folder');
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(subfolders).toHaveLength(1);
|
||||
|
||||
// Check request name
|
||||
expect(requests[0].name).toBe('Get by ID');
|
||||
expect(requests[0].request.url).toBe('{{baseUrl}}/:id');
|
||||
|
||||
// Check subfolder
|
||||
expect(subfolders[0].name).toBe('{subId}');
|
||||
expect(subfolders[0].type).toBe('folder');
|
||||
expect(subfolders[0].items).toHaveLength(1);
|
||||
expect(subfolders[0].items[0].name).toBe('Get by ID and sub ID');
|
||||
expect(subfolders[0].items[0].request.url).toBe('{{baseUrl}}/:id/:subId');
|
||||
});
|
||||
|
||||
it('should handle tag based grouping', () => {
|
||||
const result = openApiToBruno(openApiSpec, { groupBy: 'tags' });
|
||||
|
||||
// With tags grouping, requests without tags should be ungrouped
|
||||
expect(result.items).toHaveLength(2);
|
||||
|
||||
// Both should be individual requests (not in folders)
|
||||
result.items.forEach((item) => {
|
||||
expect(item.type).toBe('http-request');
|
||||
});
|
||||
|
||||
// Check request names
|
||||
const requestNames = result.items.map((req) => req.name);
|
||||
expect(requestNames).toContain('Get by ID');
|
||||
expect(requestNames).toContain('Get by ID and sub ID');
|
||||
|
||||
// Check request URLs
|
||||
const urls = result.items.map((req) => req.request.url);
|
||||
expect(urls).toContain('{{baseUrl}}/:id');
|
||||
expect(urls).toContain('{{baseUrl}}/:id/:subId');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('OpenAPI Path-Based Grouping - Duplicate Names', () => {
|
||||
it('should not add suffixes to duplicate operation names in different folders', () => {
|
||||
const openApiSpec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Duplicate Names Test API',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.example.com/v1'
|
||||
}
|
||||
],
|
||||
paths: {
|
||||
// Users folder - should have "Get User Details" operation
|
||||
'/users/{id}': {
|
||||
get: {
|
||||
summary: 'Get User Details',
|
||||
operationId: 'getUserDetails',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'User details'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Products folder - should also have "Get User Details" operation (different context)
|
||||
'/products/{id}/owner': {
|
||||
get: {
|
||||
summary: 'Get User Details',
|
||||
operationId: 'getProductOwnerDetails',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Product owner details'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Orders folder - should also have "Get User Details" operation (different context)
|
||||
'/orders/{orderId}/customer': {
|
||||
get: {
|
||||
summary: 'Get User Details',
|
||||
operationId: 'getOrderCustomerDetails',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Order customer details'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = openApiToBruno(openApiSpec, { groupBy: 'path' });
|
||||
|
||||
// Find the folders
|
||||
const usersFolder = result.items.find((item) => item.name === 'users');
|
||||
const productsFolder = result.items.find((item) => item.name === 'products');
|
||||
const ordersFolder = result.items.find((item) => item.name === 'orders');
|
||||
|
||||
// Find requests in each folder
|
||||
// Users folder: /users/{id} -> should have request directly in users folder
|
||||
const usersIdFolder = usersFolder.items.find((item) => item.name === '{id}');
|
||||
expect(usersIdFolder).toBeDefined();
|
||||
const getUserDetailsRequest = usersIdFolder.items.find((item) => item.type === 'http-request');
|
||||
|
||||
// Products folder: /products/{id}/owner -> should have request in products/{id}/owner
|
||||
const productsIdFolder = productsFolder.items.find((item) => item.name === '{id}');
|
||||
expect(productsIdFolder).toBeDefined();
|
||||
const productsOwnerFolder = productsIdFolder.items.find((item) => item.name === 'owner');
|
||||
expect(productsOwnerFolder).toBeDefined();
|
||||
const getProductOwnerRequest = productsOwnerFolder.items.find((item) => item.type === 'http-request');
|
||||
|
||||
// Orders folder: /orders/{orderId}/customer -> should have request in orders/{orderId}/customer
|
||||
const ordersIdFolder = ordersFolder.items.find((item) => item.name === '{orderId}');
|
||||
expect(ordersIdFolder).toBeDefined();
|
||||
const ordersCustomerFolder = ordersIdFolder.items.find((item) => item.name === 'customer');
|
||||
expect(ordersCustomerFolder).toBeDefined();
|
||||
const getOrderCustomerRequest = ordersCustomerFolder.items.find((item) => item.type === 'http-request');
|
||||
|
||||
expect(getUserDetailsRequest).toBeDefined();
|
||||
expect(getProductOwnerRequest).toBeDefined();
|
||||
expect(getOrderCustomerRequest).toBeDefined();
|
||||
|
||||
// CRITICAL ASSERTIONS: Names should NOT have suffixes
|
||||
// Each folder should have its own namespace, so duplicate names across folders should be allowed
|
||||
|
||||
// All requests should have clean names (NO suffixes like "(GET)" or "(1)")
|
||||
expect(getUserDetailsRequest.name).toBe('Get User Details');
|
||||
expect(getProductOwnerRequest.name).toBe('Get User Details');
|
||||
expect(getOrderCustomerRequest.name).toBe('Get User Details');
|
||||
});
|
||||
});
|
||||
212
tests/import/openapi/cli/fixtures/openapi.json
Normal file
212
tests/import/openapi/cli/fixtures/openapi.json
Normal file
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Simple Test API",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple API for testing groupBy functionality"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com",
|
||||
"description": "Example server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users",
|
||||
"operationId": "getUsers",
|
||||
"tags": ["users"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of users",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create user",
|
||||
"operationId": "createUser",
|
||||
"tags": ["users"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"summary": "Get user by ID",
|
||||
"operationId": "getUserById",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User details"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update user",
|
||||
"operationId": "updateUser",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products": {
|
||||
"get": {
|
||||
"summary": "Get all products",
|
||||
"operationId": "getProducts",
|
||||
"tags": ["products"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of products"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products/{id}": {
|
||||
"get": {
|
||||
"summary": "Get product by ID",
|
||||
"operationId": "getProductById",
|
||||
"tags": ["products"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Product details"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orders": {
|
||||
"get": {
|
||||
"summary": "Get all orders",
|
||||
"operationId": "getOrders",
|
||||
"tags": ["orders"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of orders"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create order",
|
||||
"operationId": "createOrder",
|
||||
"tags": ["orders"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": { "type": "integer" },
|
||||
"productId": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Order created"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orders/{id}": {
|
||||
"get": {
|
||||
"summary": "Get order by ID",
|
||||
"operationId": "getOrderById",
|
||||
"tags": ["orders"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order details"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
tests/import/openapi/cli/group-by-import.spec.ts
Normal file
78
tests/import/openapi/cli/group-by-import.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from '../../../../playwright';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('OpenAPI Import GroupBy Tests', () => {
|
||||
test('CLI: Import OpenAPI with tags grouping', async ({ createTmpDir }) => {
|
||||
const outputDir = await createTmpDir('openapi-tags');
|
||||
const jsonOutputPath = path.join(outputDir, 'petstore-tags.json');
|
||||
|
||||
// Run OpenAPI import with tags grouping using JSON output
|
||||
const cliPath = path.resolve(__dirname, '../../../../packages/bruno-cli/bin/bru.js');
|
||||
const specPath = path.resolve(__dirname, './fixtures/openapi.json');
|
||||
const command = `node "${cliPath}" import openapi --source "${specPath}" --output-file "${jsonOutputPath}" --collection-name "Simple API (Tags)" --group-by tags`;
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
// Continue with test even if import fails
|
||||
}
|
||||
|
||||
// Verify JSON file was created
|
||||
expect(fs.existsSync(jsonOutputPath)).toBe(true);
|
||||
|
||||
// Read and verify collection structure
|
||||
const jsonCollection = JSON.parse(fs.readFileSync(jsonOutputPath, 'utf8'));
|
||||
expect(jsonCollection.name).toBe('Simple API (Tags)');
|
||||
|
||||
// Verify tags grouping creates folders by OpenAPI tags
|
||||
const folders = jsonCollection.items.filter((item) => item.type === 'folder');
|
||||
expect(folders.length).toBe(3);
|
||||
|
||||
const folderNames = folders.map((folder) => folder.name);
|
||||
expect(folderNames).toContain('users');
|
||||
expect(folderNames).toContain('products');
|
||||
expect(folderNames).toContain('orders');
|
||||
|
||||
// Verify tags grouping doesn't create {id} folders
|
||||
const hasIdFolders = folders.some((folder) => folder.items?.some((item) => item.name === '{id}'));
|
||||
expect(hasIdFolders).toBe(false);
|
||||
});
|
||||
|
||||
test('CLI: Import OpenAPI with path grouping', async ({ createTmpDir }) => {
|
||||
const outputDir = await createTmpDir('openapi-path');
|
||||
const jsonOutputPath = path.join(outputDir, 'petstore-path.json');
|
||||
|
||||
// Run OpenAPI import with path grouping using JSON output
|
||||
const cliPath = path.resolve(__dirname, '../../../../packages/bruno-cli/bin/bru.js');
|
||||
const specPath = path.resolve(__dirname, './fixtures/openapi.json');
|
||||
const command = `node "${cliPath}" import openapi --source "${specPath}" --output-file "${jsonOutputPath}" --collection-name "Simple API (Path)" --group-by path`;
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
// Continue with test even if import fails
|
||||
}
|
||||
|
||||
// Verify JSON file was created
|
||||
expect(fs.existsSync(jsonOutputPath)).toBe(true);
|
||||
|
||||
// Read and verify collection structure
|
||||
const jsonCollection = JSON.parse(fs.readFileSync(jsonOutputPath, 'utf8'));
|
||||
expect(jsonCollection.name).toBe('Simple API (Path)');
|
||||
|
||||
// Verify path grouping creates folders by URL path structure
|
||||
const folders = jsonCollection.items.filter((item) => item.type === 'folder');
|
||||
expect(folders.length).toBe(3); // users, products, orders
|
||||
|
||||
const folderNames = folders.map((folder) => folder.name);
|
||||
expect(folderNames).toContain('users');
|
||||
expect(folderNames).toContain('products');
|
||||
expect(folderNames).toContain('orders');
|
||||
|
||||
// Verify path grouping creates {id} folders for parameterized paths
|
||||
const hasIdFolders = folders.some((folder) => folder.items?.some((item) => item.name === '{id}'));
|
||||
expect(hasIdFolders).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,13 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('OpenAPI Duplicate Names Handling', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('should handle duplicate operation names', async ({ page, createTmpDir }) => {
|
||||
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-duplicate-operation-name.yaml');
|
||||
|
||||
@@ -18,6 +24,13 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
|
||||
// wait for the file processing to complete
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// verify that the collection location modal appears (OpenAPI files go directly to location modal)
|
||||
const locationModal = page.getByTestId('import-collection-location-modal');
|
||||
// verify the collection name is correctly parsed despite duplicate operation names
|
||||
@@ -37,14 +50,5 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
|
||||
|
||||
// verify that all 3 requests were imported correctly despite duplicate operation names
|
||||
await expect(page.locator('#collection-duplicate-test-collection .collection-item-name')).toHaveCount(3);
|
||||
|
||||
// cleanup: close the collection
|
||||
await page
|
||||
.locator('.collection-name')
|
||||
.filter({ has: page.locator('#sidebar-collection-name:has-text("Duplicate Test Collection")') })
|
||||
.locator('.collection-actions')
|
||||
.click();
|
||||
await page.locator('.dropdown-item').getByText('Close').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
76
tests/import/openapi/fixtures/openapi-path-grouping.json
Normal file
76
tests/import/openapi/fixtures/openapi-path-grouping.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Path Grouping Test API",
|
||||
"description": "API for testing path-based folder grouping",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com",
|
||||
"description": "Test server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "List users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"summary": "Get user by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products": {
|
||||
"get": {
|
||||
"summary": "List products",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products/{id}": {
|
||||
"get": {
|
||||
"summary": "Get product by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,13 @@ test.describe('Import OpenAPI v3 JSON Collection', () => {
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Verify that the Import Collection modal is displayed (for location selection)
|
||||
const locationModal = page.getByRole('dialog');
|
||||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||||
|
||||
@@ -14,6 +14,13 @@ test.describe('Import OpenAPI v3 YAML Collection', () => {
|
||||
|
||||
await page.setInputFiles('input[type="file"]', openApiFile);
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('OpenAPI Newline Handling', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('should handle operation names with newlines', async ({ page, createTmpDir }) => {
|
||||
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-newline-in-operation-name.yaml');
|
||||
|
||||
@@ -15,6 +21,13 @@ test.describe('OpenAPI Newline Handling', () => {
|
||||
// upload the OpenAPI file with problematic operation names
|
||||
await page.setInputFiles('input[type="file"]', openApiFile);
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// wait for the file processing to complete
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
@@ -37,14 +50,5 @@ test.describe('OpenAPI Newline Handling', () => {
|
||||
// verify that all requests were imported correctly despite newlines in operation names
|
||||
// the parser should clean up the operation names and create valid request names
|
||||
await expect(page.locator('#collection-newline-test-collection .collection-item-name')).toHaveCount(2);
|
||||
|
||||
// cleanup: close the collection
|
||||
await page
|
||||
.locator('.collection-name')
|
||||
.filter({ has: page.locator('#sidebar-collection-name:has-text("Newline Test Collection")') })
|
||||
.locator('.collection-actions')
|
||||
.click();
|
||||
await page.locator('.dropdown-item').getByText('Close').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
68
tests/import/openapi/path-based-grouping.spec.ts
Normal file
68
tests/import/openapi/path-based-grouping.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('OpenAPI Path-Based Grouping', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('should import with path-based folder grouping', async ({ page, createTmpDir }) => {
|
||||
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-path-grouping.json');
|
||||
|
||||
// Start the import process
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
|
||||
// Wait for import collection modal to be ready
|
||||
const importModal = page.getByTestId('import-collection-modal');
|
||||
await importModal.waitFor({ state: 'visible' });
|
||||
|
||||
// Upload the OpenAPI file
|
||||
await page.setInputFiles('input[type="file"]', openApiFile);
|
||||
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// Select path-based grouping from the dropdown
|
||||
await settingsModal.getByTestId('grouping-dropdown').click();
|
||||
|
||||
// Wait for dropdown options to be visible (they might be rendered outside the modal)
|
||||
await page.getByTestId('grouping-option-path').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('grouping-option-path').click();
|
||||
|
||||
// Now import the collection with path-based grouping
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Verify that the collection location modal appears
|
||||
const locationModal = page.getByTestId('import-collection-location-modal');
|
||||
await expect(locationModal.getByText('Path Grouping Test API')).toBeVisible();
|
||||
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('path-grouping-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify the collection was imported successfully
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Path Grouping Test API')).toBeVisible();
|
||||
|
||||
// Configure the collection settings
|
||||
await page.locator('#sidebar-collection-name').getByText('Path Grouping Test API').click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify path-based folder structure was created
|
||||
// Should have 'users' and 'products' folders
|
||||
await expect(page.locator('.collection-item-name').getByText('users')).toBeVisible();
|
||||
await expect(page.locator('.collection-item-name').getByText('products')).toBeVisible();
|
||||
|
||||
// Expand the products folder to check for nested structure
|
||||
await page.locator('.collection-item-name').getByText('products').click();
|
||||
|
||||
// Verify that the products folder contains the {id} subfolder
|
||||
await expect(page.locator('.collection-item-name').getByText('{id}')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user