feat(import): enhance import functionality with issue tracking and logging (#8098)

* feat: enhance import functionality with issue tracking and logging

- Updated the import process to return both collections and issues for better error handling.
- Introduced a new toast notification for displaying import issues, allowing users to copy or report them.
- Enhanced logging for import issues, capturing errors and warnings during the import process.
- Added new components for actionable toasts and import issues display.
- Updated tests to validate the new import behavior and issue tracking.

* feat: enhance import issues handling with new toast notifications and tests

- Added optional testId prop to ActionableToast for better test targeting.
- Updated ImportIssuesToast to include data-testid attributes for improved e2e testing.
- Introduced a new Postman collection fixture to test partial import scenarios.
- Created new tests to validate the import process, including issue reporting and copying functionality.
- Implemented utility functions to manage import issues toasts during tests.

* fix: improve clipboard copy functionality and handle import issues more robustly

- Updated BulkImportCollectionLocation to always set import issues, ensuring consistent state management.
- Enhanced clipboard copy functionality in ImportIssuesToast and BulkImportCollectionLocation to handle errors gracefully with user feedback.
- Added aria-label for better accessibility in ActionableToast close button.

* refactor: enhance import issue logging and toast notifications

- Improved logging in BulkImportCollectionLocation and ImportCollectionLocation to provide detailed summaries of import issues, including counts of skipped items and warnings.
- Updated ImportIssuesToast to handle long issue descriptions and provide user feedback for copying issue details to the clipboard.
- Removed ActionableToast component and its styles, consolidating toast functionality within ImportIssuesToast for better maintainability.
- Enhanced styling for ImportIssuesToast to improve user experience and accessibility.

* refactor: update logging level for import issues in BulkImportCollectionLocation and ImportCollectionLocation

- Changed log type from 'error' to 'warn' for import issue summaries in both components to better reflect the severity of the messages.
- This adjustment improves clarity in the logging system and aligns with the intended handling of import warnings.

* feat: enhance ImportIssuesToast with URL length warning and styling improvements

- Added an alert icon and improved styling for the URL-too-long warning in ImportIssuesToast to enhance user experience.
- Introduced a new test for verifying the display of the URL length warning when importing collections with many issues.
- Updated locators to include a test ID for the URL-too-long warning, facilitating better end-to-end testing.

* style: update ImportIssuesToast styling for improved user experience

- Changed background and border colors in StyledWrapper for better visual consistency.
- Enhanced box-shadow and close button styles for improved accessibility and interaction.
- Adjusted padding and gap in warning messages for better layout and readability.
This commit is contained in:
sanish chirayath
2026-05-28 19:41:03 +05:30
committed by GitHub
parent 49088e98c8
commit 244f528277
20 changed files with 1791 additions and 434 deletions

View File

@@ -10,6 +10,7 @@ import { IconX, IconLoader2, IconCheck, IconCaretDown } from '@tabler/icons';
import InfoTip from 'components/InfoTip/index';
import Help from 'components/Help';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { addLog } from 'providers/ReduxStore/slices/logs';
import Dropdown from 'components/Dropdown';
import SelectionList from 'components/SelectionList';
import { postmanToBruno } from 'utils/importers/postman-collection';
@@ -19,6 +20,7 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { wsdlToBruno } from '@usebruno/converters';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { showImportIssuesToast } from 'components/Toast/ImportIssuesToast';
import get from 'lodash/get';
const STATUS = {
@@ -66,8 +68,10 @@ const getCollectionName = (format, rawData) => {
};
// Convert raw data to Bruno collection format
// Returns { collection, issues } where issues tracks items that were skipped or degraded
const convertCollection = async (format, rawData, groupingType) => {
let collection;
let issues = [];
switch (format) {
case 'openapi':
@@ -76,9 +80,12 @@ const convertCollection = async (format, rawData, groupingType) => {
case 'wsdl':
collection = await wsdlToBruno(rawData);
break;
case 'postman':
collection = await postmanToBruno(rawData);
case 'postman': {
const result = await postmanToBruno(rawData);
collection = result.collection;
issues = result.issues || [];
break;
}
case 'insomnia':
collection = convertInsomniaToBruno(rawData);
break;
@@ -89,7 +96,7 @@ const convertCollection = async (format, rawData, groupingType) => {
throw new Error('Unknown collection format');
}
return collection;
return { collection, issues };
};
export function normalizeName(name) {
@@ -150,6 +157,7 @@ export const BulkImportCollectionLocation = ({
const [collectionFormat, setCollectionFormat] = useState('bru');
const [renamedCollectionNames, setRenamedCollectionNames] = useState({});
const [renamedEnvironmentNames, setRenamedEnvironmentNames] = useState({});
const [importIssues, setImportIssues] = useState({});
// Extract data based on import type
const importType = importData?.type;
@@ -160,6 +168,21 @@ export const BulkImportCollectionLocation = ({
const importedCollectionFromBulk = isBulkImport ? importData.collection : [];
const importedEnvironmentFromBulk = isBulkImport ? (importData.environment || []) : [];
// Extract per-collection issues from bulk import data
useEffect(() => {
if (isBulkImport && importData.issues) {
const issuesMap = {};
importData.issues.forEach((entry, index) => {
if (entry.issues && entry.issues.length > 0 && importedCollectionFromBulk[index]) {
issuesMap[importedCollectionFromBulk[index].uid] = entry.issues;
}
});
setImportIssues(issuesMap);
} else {
setImportIssues({});
}
}, [isBulkImport, importData]);
// For multiple files import
const filesData = isMultipleImport ? importData.filesData : [];
const hasOpenApiSpec = filesData.some((f) => f.type === 'openapi');
@@ -281,19 +304,53 @@ export const BulkImportCollectionLocation = ({
if (isMultipleImport) {
// Convert selected files to collections at submit time
const collectedIssues = {};
for (const item of selectedItems) {
try {
const collection = await convertCollection(item._fileData.type, item._fileData.data, groupingType);
const { collection, issues } = await convertCollection(item._fileData.type, item._fileData.data, groupingType);
if (collection) {
// Preserve the synthetic UID so status tracking, rename tracking,
// and UI rendering all use the same key
collection.uid = item.uid;
filteredCollections.push(collection);
if (issues && issues.length > 0) {
collectedIssues[item.uid] = issues;
}
}
} catch (err) {
console.warn(`Failed to convert file ${item._fileData.file.name}:`, err);
}
}
if (Object.keys(collectedIssues).length > 0) {
setImportIssues(collectedIssues);
const allIssues = [];
const timestamp = new Date().toISOString();
Object.entries(collectedIssues).forEach(([uid, issues]) => {
const item = selectedItems.find((s) => s.uid === uid);
const name = item?.name || uid;
const skipped = issues.filter((i) => i.severity === 'error').length;
const warnings = issues.filter((i) => i.severity === 'warning').length;
const parts = [];
if (skipped > 0) parts.push(`skipped ${skipped} item(s)`);
if (warnings > 0) parts.push(`${warnings} warning(s)`);
// Per-collection summary header
dispatch(addLog({ type: 'warn', args: [`Import: ${name}${parts.join(', ')}`], timestamp }));
// Individual issues for this collection
issues.forEach((issue) => {
allIssues.push({ ...issue, path: `${name} > ${issue.path}` });
const logType = issue.severity === 'error' ? 'error' : 'warn';
const logArgs = [`[${issue.path}] ${issue.message}`];
if (issue.sourceItem) logArgs.push(issue.sourceItem);
dispatch(addLog({ type: logType, args: logArgs, timestamp }));
});
});
// Single toast for all collections
showImportIssuesToast(allIssues);
}
} else if (isBulkImport) {
// For bulk import, use selected collections directly
filteredCollections = selectedItems;
@@ -608,6 +665,29 @@ export const BulkImportCollectionLocation = ({
See error
</button>
)}
{status[collection.uid] === STATUS.SUCCESS && importIssues[collection.uid] && (
<div className="flex items-center gap-2">
<span className="text-yellow-600 text-xs">
{importIssues[collection.uid].filter((i) => i.severity === 'error').length} item(s) skipped
</span>
<button
onClick={async () => {
const text = importIssues[collection.uid]
.map((i) => `[${i.severity.toUpperCase()}] ${i.path}${i.message}`)
.join('\n');
try {
await navigator.clipboard.writeText(text);
toast.success('Copied to clipboard', { duration: 2000 });
} catch (err) {
toast.error('Failed to copy to clipboard', { duration: 3000 });
}
}}
className="text-yellow-600 text-xs hover:underline"
>
Copy
</button>
</div>
)}
</div>
))}
</div>

View File

@@ -13,11 +13,13 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { processOpenCollection } from 'utils/importers/opencollection';
import { wsdlToBruno } from '@usebruno/converters';
import { toastError } from 'utils/common/error';
import { addLog } from 'providers/ReduxStore/slices/logs';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import Modal from 'components/Modal';
import Help from 'components/Help';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
import { showImportIssuesToast } from 'components/Toast/ImportIssuesToast';
import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';
// Extract collection name from raw data
@@ -53,9 +55,11 @@ const getCollectionName = (format, rawData) => {
};
// Convert raw data to Bruno collection format
// Returns { collection, issues } where issues tracks items that were skipped or degraded
const convertCollection = async (format, rawData, groupingType, collectionFormat) => {
try {
let collection;
let issues = [];
switch (format) {
case 'openapi':
@@ -64,9 +68,12 @@ const convertCollection = async (format, rawData, groupingType, collectionFormat
case 'wsdl':
collection = await wsdlToBruno(rawData);
break;
case 'postman':
collection = await postmanToBruno(rawData);
case 'postman': {
const result = await postmanToBruno(rawData);
collection = result.collection;
issues = result.issues || [];
break;
}
case 'insomnia':
collection = convertInsomniaToBruno(rawData);
break;
@@ -84,7 +91,7 @@ const convertCollection = async (format, rawData, groupingType, collectionFormat
throw new Error('Unknown collection format');
}
return collection;
return { collection, issues };
} catch (err) {
console.error('Conversion error:', err);
toastError(err, 'Failed to convert collection');
@@ -135,7 +142,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
.required('Location is required')
}),
onSubmit: async (values) => {
const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat);
const { collection: convertedCollection, issues } = await convertCollection(format, rawData, groupingType, collectionFormat);
const options = { format: collectionFormat };
if (showCheckForSpecUpdatesOption && enableCheckForSpecUpdates) {
@@ -164,6 +171,26 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
}
handleSubmit(convertedCollection, values.collectionLocation, options);
if (issues && issues.length > 0) {
// Show toast with copy/report actions
showImportIssuesToast(issues);
// Log each issue to Bruno's internal console
const skipped = issues.filter((i) => i.severity === 'error').length;
const warnings = issues.filter((i) => i.severity === 'warning').length;
const parts = [];
if (skipped > 0) parts.push(`skipped ${skipped} item(s)`);
if (warnings > 0) parts.push(`${warnings} warning(s)`);
const timestamp = new Date().toISOString();
dispatch(addLog({ type: 'warn', args: [`Import: ${collectionName}${parts.join(', ')}`], timestamp }));
issues.forEach((issue) => {
const logType = issue.severity === 'error' ? 'error' : 'warn';
const logArgs = [`[${issue.path}] ${issue.message}`];
if (issue.sourceItem) logArgs.push(issue.sourceItem);
dispatch(addLog({ type: logType, args: logArgs, timestamp }));
});
}
}
});

View File

@@ -0,0 +1,143 @@
import styled from 'styled-components';
import { rgba } from 'polished';
const StyledWrapper = styled.div`
position: relative;
display: flex;
background: ${(props) => props.theme.background.base};
color: ${(props) => props.theme.text};
border: 1px solid ${(props) => props.theme.border.border2};
border-radius: ${(props) => props.theme.border.radius.md};
overflow: hidden;
max-width: 420px;
min-width: 380px;
margin-bottom: 2rem;
margin-right: 0.75rem;
box-shadow: ${(props) => props.theme.shadow.lg};
transition: all 0.3s ease;
.toast-accent {
width: 4px;
flex-shrink: 0;
border-radius: ${(props) => props.theme.border.radius.md} 0 0 ${(props) => props.theme.border.radius.md};
background: ${(props) => props.theme.colors.text.danger};
}
.toast-body {
flex: 1;
padding: 12px 14px;
}
.toast-close {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: none;
border: none;
cursor: pointer;
color: ${(props) => props.theme.text};
border-radius: ${(props) => props.theme.border.radius.sm};
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease;
&:hover {
opacity: 1;
background-color: ${(props) => rgba(props.theme.text, 0.1)};
}
}
.toast-title {
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
color: ${(props) => props.theme.text};
}
.toast-hint {
font-size: 12px;
color: ${(props) => props.theme.colors.text.subtext1};
margin-bottom: 8px;
}
.toast-checkbox {
display: flex;
align-items: flex-start;
gap: 6px;
cursor: pointer;
margin-bottom: 10px;
input[type='checkbox'] {
accent-color: ${(props) => props.theme.primary.solid};
cursor: pointer;
margin: 0;
margin-top: 2px;
flex-shrink: 0;
}
.toast-checkbox-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.toast-checkbox-label {
font-size: 12px;
color: ${(props) => props.theme.text};
}
.toast-checkbox-desc {
font-size: 11px;
color: ${(props) => props.theme.colors.text.subtext2};
line-height: 1.4;
}
}
.toast-warning {
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 11px;
color: ${(props) => props.theme.status.warning.text};
background: ${(props) => props.theme.status.warning.background};
border: 1px solid ${(props) => props.theme.status.warning.border};
border-radius: ${(props) => props.theme.border.radius.sm};
padding: 6px 8px;
margin-bottom: 8px;
line-height: 1.4;
.toast-warning-icon {
flex-shrink: 0;
margin-top: 1px;
}
}
.toast-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.toast-btn {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
padding: 4px 10px;
cursor: pointer;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.sm};
background: ${(props) => props.theme.background.surface1};
color: ${(props) => props.theme.text};
&:hover {
background: ${(props) => props.theme.background.surface2};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,176 @@
import React, { useState, useMemo } from 'react';
import toast from 'react-hot-toast';
import { IconAlertCircle, IconBrandGithub, IconCopy, IconX } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const GITHUB_ISSUES_URL = 'https://github.com/usebruno/bruno/issues/new';
const MAX_URL_LENGTH = 8000;
const ImportIssuesToastContent = ({ t, issues, summary }) => {
const [includeItems, setIncludeItems] = useState(false);
const hasSourceItems = issues.some((i) => i.sourceItem);
const issuesSummary = issues.map((i) => `[${i.severity.toUpperCase()}] ${i.path}${i.message}`).join('\n');
const buildIssueBody = () => {
const sections = [
'### Description',
'Postman collection import completed with issues. Some items could not be converted.',
'',
'### Import Issues',
'```',
issuesSummary,
'```'
];
if (includeItems) {
const itemsWithSource = issues.filter((i) => i.sourceItem);
if (itemsWithSource.length > 0) {
const itemsJson = itemsWithSource
.map((i) => `// ${i.path}\n${JSON.stringify(i.sourceItem, null, 2)}`)
.join('\n\n');
sections.push(
'',
'### Failed Items',
'> **Please redact any sensitive information (API keys, tokens, passwords, internal URLs) before submitting.**',
'```json',
itemsJson,
'```'
);
}
}
sections.push(
'',
'### Steps to Reproduce',
'1. Import the attached Postman collection (redact sensitive data before attaching)',
'2. ',
'',
'### Additional Context',
''
);
return sections.join('\n');
};
const isUrlTooLong = useMemo(() => {
const title = `Postman import: ${summary}`;
const body = buildIssueBody();
const params = new URLSearchParams({ title, body, labels: 'bug' });
return `${GITHUB_ISSUES_URL}?${params.toString()}`.length > MAX_URL_LENGTH;
}, [issues, summary, includeItems]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(issuesSummary);
toast.success('Copied to clipboard', { duration: 2000 });
} catch (err) {
toast.error('Failed to copy to clipboard', { duration: 3000 });
}
};
const handleReport = async () => {
const title = `Postman import: ${summary}`;
const body = buildIssueBody();
if (!isUrlTooLong) {
const params = new URLSearchParams({ title, body, labels: 'bug' });
window.open(`${GITHUB_ISSUES_URL}?${params.toString()}`, '_blank');
return;
}
try {
await navigator.clipboard.writeText(body);
toast.success('Issue details copied — paste them into the GitHub issue body', { duration: 5000 });
} catch (err) {
toast.error('Failed to copy to clipboard', { duration: 3000 });
}
const params = new URLSearchParams({ title, labels: 'bug' });
window.open(`${GITHUB_ISSUES_URL}?${params.toString()}`, '_blank');
};
return (
<StyledWrapper
data-testid="import-issues-toast"
style={{
opacity: t.visible ? 1 : 0,
transform: t.visible ? 'translateX(0)' : 'translateX(100%)'
}}
>
<div className="toast-accent" />
<div className="toast-body">
<button
type="button"
className="toast-close"
aria-label="Close toast"
data-testid="import-issues-toast-close"
onClick={() => toast.dismiss(t.id)}
>
<IconX size={14} />
</button>
<div className="toast-title" data-testid="import-issues-toast-title">Imported with issues: {summary}</div>
<div className="toast-hint">Open DevTools console to see which items failed and why.</div>
{hasSourceItems && (
<label className="toast-checkbox">
<input
type="checkbox"
checked={includeItems}
onChange={(e) => setIncludeItems(e.target.checked)}
data-testid="import-issues-include-items-checkbox"
/>
<div className="toast-checkbox-text">
<span className="toast-checkbox-label">Include failed request data</span>
<span className="toast-checkbox-desc">Attaches the raw Postman request items that failed. May contain API keys, tokens, or internal URLs.</span>
</div>
</label>
)}
{isUrlTooLong && (
<div className="toast-warning" data-testid="import-issues-url-too-long-warning">
<IconAlertCircle size={14} className="toast-warning-icon" />
<span>Issue details are too long to embed in the URL. Clicking &quot;Report on GitHub&quot; will copy them to your clipboard paste it once the GitHub issue page opens.</span>
</div>
)}
<div className="toast-actions">
<button className="toast-btn" onClick={handleReport} data-testid="import-issues-report-btn">
<IconBrandGithub size={13} />
Report on GitHub
</button>
<button className="toast-btn" onClick={handleCopy} data-testid="import-issues-copy-btn">
<IconCopy size={13} />
Copy Issues
</button>
</div>
</div>
</StyledWrapper>
);
};
/**
* Show an import issues toast in the bottom-right corner.
* Aggregates all issues into a single toast — does not stack.
*/
let activeImportToastId = null;
export const showImportIssuesToast = (issues) => {
if (activeImportToastId) {
toast.dismiss(activeImportToastId);
}
const errors = issues.filter((i) => i.severity === 'error');
const warnings = issues.filter((i) => i.severity === 'warning');
const parts = [];
if (errors.length > 0) parts.push(`${errors.length} item(s) skipped`);
if (warnings.length > 0) parts.push(`${warnings.length} warning(s)`);
const summary = parts.join(', ');
activeImportToastId = toast.custom(
(t) => (
<ImportIssuesToastContent t={t} issues={issues} summary={summary} />
),
{ duration: Infinity, position: 'bottom-right' }
);
return activeImportToastId;
};
export default ImportIssuesToastContent;

View File

@@ -363,12 +363,20 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
}
};
const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap) => {
const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap, issues = [], parentPath = '') => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
item.forEach((i, index) => {
if (typeof i !== 'object' || i === null) {
issues.push({ path: parentPath ? `${parentPath} / Item ${index + 1}` : `Item ${index + 1}`, severity: 'error', message: 'Malformed collection item (not an object)' });
return;
}
const itemName = i.name || `Item ${index + 1}`;
const itemPath = parentPath ? `${parentPath} / ${itemName}` : itemName;
if (isItemAFolder(i)) {
const baseFolderName = i.name || 'Untitled Folder';
let folderName = baseFolderName;
@@ -415,7 +423,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
processAuth(i.auth, brunoFolderItem.root.request);
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap);
importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap, issues, itemPath);
}
if (i.event) {
@@ -431,377 +439,381 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
folderMap[folderName] = brunoFolderItem;
} else if (i.request) {
const method = i?.request?.method?.toUpperCase();
if (!method || typeof method !== 'string' || !method.trim()) {
console.warn('Missing or invalid request.method', method);
const rawMethod = i?.request?.method;
if (!rawMethod || typeof rawMethod !== 'string' || !rawMethod.trim()) {
issues.push({ path: itemPath, severity: 'error', message: 'Missing or invalid request method', sourceItem: i });
return;
}
const method = rawMethod.toUpperCase();
const baseRequestName = i.name || 'Untitled Request';
let requestName = baseRequestName;
let count = 1;
try {
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: method,
auth: {
mode: 'inherit',
basic: null,
bearer: null,
awsv4: null,
apikey: null,
oauth1: null,
oauth2: null,
digest: null
},
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
},
docs: transformDescription(i.request.description)
}
};
const settings = {
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
};
// Handle followRedirects setting
if (i.protocolProfileBehavior?.followRedirects !== undefined) {
settings.followRedirects = i.protocolProfileBehavior.followRedirects;
}
// Handle maxRedirects setting
if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
}
brunoRequestItem.settings = settings;
brunoParent.items.push(brunoRequestItem);
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) {
brunoRequestItem.request.script = {};
}
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?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.res = '';
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) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
while (requestMap[requestName]) {
requestName = `${baseRequestName}_${count}`;
count++;
}
if (bodyMode === 'urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(i.request.body.urlencoded, (param) => {
if (param.key == null && param.value == null) return;
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
const url = constructUrl(i.request.url);
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(normalizeHeaders(i.request.header), (header) => {
if (header.key == null && header.value == null) return;
brunoRequestItem.request.headers.push({
const brunoRequestItem = {
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
// Request-level auth
processAuth(i.request.auth, brunoRequestItem.request);
each(get(i, 'request.url.query'), (param) => {
if (param.key == null && param.value == null) {
return;
}
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(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: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
// Handle Postman examples (responses)
if (i.response && Array.isArray(i.response)) {
brunoRequestItem.examples = [];
i.response.forEach((response, responseIndex) => {
const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
const exampleName = sanitized || `Example ${responseIndex + 1}`;
// Convert originalRequest to Bruno request format
const originalRequest = response.originalRequest || {};
const exampleUrl = constructUrl(originalRequest.url);
const exampleMethod = originalRequest.method?.toUpperCase() || method;
const example = {
uid: uuid(),
itemUid: brunoRequestItem.uid,
name: exampleName,
description: '',
type: 'http-request',
request: {
url: exampleUrl,
method: exampleMethod,
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
}
name: requestName,
type: 'http-request',
seq: index + 1,
request: {
url: url,
method: method,
auth: {
mode: 'inherit',
basic: null,
bearer: null,
awsv4: null,
apikey: null,
oauth1: null,
oauth2: null,
digest: null
},
response: {
status: response.code || null,
statusText: response.status || '',
headers: [],
body: {
type: getBodyTypeFromContentTypeHeader(response.header),
content: response.body || ''
}
}
};
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
},
docs: transformDescription(i.request.description)
}
};
// Convert original request headers
if (originalRequest.header) {
normalizeHeaders(originalRequest.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.request.headers.push({
const settings = {
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
};
// Handle followRedirects setting
if (i.protocolProfileBehavior?.followRedirects !== undefined) {
settings.followRedirects = i.protocolProfileBehavior.followRedirects;
}
// Handle maxRedirects setting
if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
}
brunoRequestItem.settings = settings;
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) {
brunoRequestItem.request.script = {};
}
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?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.res = '';
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) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
// Convert original request query parameters
if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
originalRequest.url.query.forEach((param) => {
if (param.key == null && param.value == null) {
return;
}
example.request.params.push({
if (bodyMode === 'urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(i.request.body.urlencoded, (param) => {
if (param.key == null && param.value == null) return;
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
}
if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
originalRequest.url.variable.forEach((param) => {
if (!param.key) return;
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
// Convert original request body
if (originalRequest.body) {
const bodyMode = originalRequest.body.mode;
if (bodyMode === 'formdata') {
example.request.body.mode = 'multipartForm';
if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
originalRequest.body.formdata.forEach((param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
example.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
} else if (bodyMode === 'urlencoded') {
example.request.body.mode = 'formUrlEncoded';
if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
originalRequest.body.urlencoded.forEach((param) => {
if (param.key == null && param.value == null) return;
example.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
} else if (bodyMode === 'raw') {
let language = get(originalRequest, 'body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(originalRequest.header || []);
}
if (language === 'json') {
example.request.body.mode = 'json';
example.request.body.json = originalRequest.body.raw;
} else if (language === 'xml') {
example.request.body.mode = 'xml';
example.request.body.xml = originalRequest.body.raw;
} else {
example.request.body.mode = 'text';
example.request.body.text = originalRequest.body.raw;
}
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;
}
}
}
// Convert response headers
if (response.header) {
normalizeHeaders(response.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.response.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: true
});
});
if (bodyMode === 'graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(normalizeHeaders(i.request.header), (header) => {
if (header.key == null && header.value == null) return;
brunoRequestItem.request.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
// Request-level auth
processAuth(i.request.auth, brunoRequestItem.request);
each(get(i, 'request.url.query'), (param) => {
if (param.key == null && param.value == null) {
return;
}
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(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.examples.push(example);
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
requestMap[requestName] = brunoRequestItem;
// Handle Postman examples (responses)
if (i.response && Array.isArray(i.response)) {
brunoRequestItem.examples = [];
i.response.forEach((response, responseIndex) => {
const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
const exampleName = sanitized || `Example ${responseIndex + 1}`;
// Convert originalRequest to Bruno request format
const originalRequest = response.originalRequest || {};
const exampleUrl = constructUrl(originalRequest.url);
const exampleMethod = originalRequest.method?.toUpperCase() || method;
const example = {
uid: uuid(),
itemUid: brunoRequestItem.uid,
name: exampleName,
description: '',
type: 'http-request',
request: {
url: exampleUrl,
method: exampleMethod,
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
}
},
response: {
status: response.code || null,
statusText: response.status || '',
headers: [],
body: {
type: getBodyTypeFromContentTypeHeader(response.header),
content: response.body || ''
}
}
};
// Convert original request headers
if (originalRequest.header) {
normalizeHeaders(originalRequest.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.request.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
}
// Convert original request query parameters
if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
originalRequest.url.query.forEach((param) => {
if (param.key == null && param.value == null) {
return;
}
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
}
if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
originalRequest.url.variable.forEach((param) => {
if (!param.key) return;
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
// Convert original request body
if (originalRequest.body) {
const bodyMode = originalRequest.body.mode;
if (bodyMode === 'formdata') {
example.request.body.mode = 'multipartForm';
if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
originalRequest.body.formdata.forEach((param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
example.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
} else if (bodyMode === 'urlencoded') {
example.request.body.mode = 'formUrlEncoded';
if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
originalRequest.body.urlencoded.forEach((param) => {
if (param.key == null && param.value == null) return;
example.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
} else if (bodyMode === 'raw') {
let language = get(originalRequest, 'body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(originalRequest.header || []);
}
if (language === 'json') {
example.request.body.mode = 'json';
example.request.body.json = originalRequest.body.raw;
} else if (language === 'xml') {
example.request.body.mode = 'xml';
example.request.body.xml = originalRequest.body.raw;
} else {
example.request.body.mode = 'text';
example.request.body.text = originalRequest.body.raw;
}
}
}
// Convert response headers
if (response.header) {
normalizeHeaders(response.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.response.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: true
});
});
}
brunoRequestItem.examples.push(example);
});
}
brunoParent.items.push(brunoRequestItem);
requestMap[requestName] = brunoRequestItem;
} catch (err) {
issues.push({ path: itemPath, severity: 'error', message: err.message, sourceItem: i });
}
}
});
};
@@ -876,8 +888,14 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
importScriptsFromEvents(collection.event, brunoCollection.root.request);
}
const issues = [];
if (collection?.variable) {
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
try {
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
} catch (err) {
issues.push({ path: 'Collection Variables', severity: 'warning', message: err.message });
}
}
// Collection level auth
@@ -886,7 +904,7 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
// Create a single scriptMap for all items
const scriptMap = useWorkers ? new Map() : null;
importPostmanV2CollectionItem(brunoCollection, collection.item, { useWorkers }, scriptMap);
importPostmanV2CollectionItem(brunoCollection, collection.item, { useWorkers }, scriptMap, issues);
// Process all scripts in a single call at the top level
if (useWorkers && scriptMap && scriptMap.size > 0) {
@@ -945,7 +963,7 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
}
}
return brunoCollection;
return { collection: brunoCollection, issues };
};
const parsePostmanCollection = async (collection, { useWorkers = false }) => {
@@ -979,13 +997,13 @@ const parsePostmanCollection = async (collection, { useWorkers = false }) => {
const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => {
try {
const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers });
const transformedCollection = transformItemsInCollection(parsedPostmanCollection);
const { collection: parsedCollection, issues } = await parsePostmanCollection(postmanCollection, { useWorkers });
const transformedCollection = transformItemsInCollection(parsedCollection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
// Apply backward compatibility transformation for string status to number
const statusTransformedCollection = transformExampleStatusInCollection(hydratedCollection);
const validatedCollection = validateSchema(statusTransformedCollection);
return validatedCollection;
return { collection: validatedCollection, issues };
} catch (err) {
console.log(err);
throw new Error(`Import collection failed: ${err.message}`);

View File

@@ -0,0 +1,210 @@
{
"info": {
"_postman_id": "test-import-issues-001",
"name": "Import Issues Test Collection",
"description": "A Postman collection designed to test partial import handling. Contains a mix of valid requests, requests with missing/invalid methods, and edge cases that should be gracefully handled.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Valid GET Request",
"request": {
"method": "GET",
"header": [
{ "key": "Accept", "value": "application/json" }
],
"url": {
"raw": "https://api.example.com/users",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users"]
}
}
},
{
"name": "Missing Method (null)",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-1",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-1"]
}
}
},
{
"name": "Missing Method (absent)",
"request": {
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-2",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-2"]
}
}
},
{
"name": "Empty String Method",
"request": {
"method": "",
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-3",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-3"]
}
}
},
{
"name": "Whitespace-Only Method",
"request": {
"method": " ",
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-4",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-4"]
}
}
},
{
"name": "Valid POST Request",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\"name\": \"test\", \"email\": \"test@example.com\"}",
"options": {
"raw": { "language": "json" }
}
},
"url": {
"raw": "https://api.example.com/users",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users"]
}
}
},
{
"name": "API Folder",
"item": [
{
"name": "Valid Nested GET",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.example.com/nested/valid",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["nested", "valid"]
}
}
},
{
"name": "Nested Missing Method",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/nested/should-be-skipped",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["nested", "should-be-skipped"]
}
}
},
{
"name": "Deep Subfolder",
"item": [
{
"name": "Deep Valid Request",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "https://api.example.com/deep/valid",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["deep", "valid"]
}
}
},
{
"name": "Deep Bad Method",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/deep/should-be-skipped",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["deep", "should-be-skipped"]
}
}
}
]
}
]
},
{
"name": "Valid PUT Request",
"request": {
"method": "PUT",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{token}}" }
],
"body": {
"mode": "raw",
"raw": "{\"name\": \"updated\"}",
"options": {
"raw": { "language": "json" }
}
},
"url": {
"raw": "https://api.example.com/users/{{userId}}",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users", "{{userId}}"],
"variable": [
{ "key": "userId", "value": "123" }
]
}
}
},
{
"name": "No Request Object At All"
},
{
"name": "Valid PATCH Request",
"request": {
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"status\": \"active\"}"
},
"url": {
"raw": "https://api.example.com/users/1/status",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users", "1", "status"]
}
}
}
],
"variable": [
{ "key": "baseUrl", "value": "https://api.example.com" },
{ "key": "token", "value": "test-token-123" }
]
}

View File

@@ -101,7 +101,7 @@ describe('Postman to Bruno Converter with Examples', () => {
};
test('should convert Postman collection with examples to Bruno format', async () => {
const brunoCollection = await postmanToBruno(postmanCollectionWithExamples);
const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection with examples');
@@ -180,7 +180,7 @@ describe('Postman to Bruno Converter with Examples', () => {
]
};
const brunoCollection = await postmanToBruno(postmanCollectionWithoutExamples);
const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithoutExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection without examples');
@@ -218,7 +218,7 @@ describe('Postman to Bruno Converter with Examples', () => {
]
};
const brunoCollection = await postmanToBruno(postmanCollectionWithEmptyExamples);
const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithEmptyExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection with empty examples');

View File

@@ -29,7 +29,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -86,7 +86,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -141,7 +141,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -200,7 +200,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'apikey',
@@ -265,7 +265,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'digest',
@@ -312,7 +312,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'basic',
@@ -360,7 +360,7 @@ describe('Collection Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'bearer',

View File

@@ -49,7 +49,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'inherit',
@@ -113,7 +113,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'none',
@@ -174,7 +174,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'basic',
@@ -233,7 +233,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'bearer',
@@ -294,7 +294,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'apikey',
@@ -360,7 +360,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'digest',
@@ -410,7 +410,7 @@ describe('Folder Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'basic',

View File

@@ -0,0 +1,199 @@
import { describe, it, expect } from '@jest/globals';
import postmanToBruno from '../../../src/postman/postman-to-bruno';
const makeCollection = (items, overrides = {}) => ({
info: {
_postman_id: 'test-id',
name: 'Test Collection',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: items,
...overrides
});
const makeRequest = (name, method = 'GET', url = 'https://example.com') => ({
name,
request: {
method,
header: [],
url: { raw: url, protocol: 'https', host: ['example', 'com'] }
}
});
describe('partial-import', () => {
it('should import valid items and skip items with missing method', async () => {
const items = [
makeRequest('Valid Request 1'),
{ name: 'Bad Request', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
makeRequest('Valid Request 2')
];
const { collection, issues } = await postmanToBruno(makeCollection(items));
expect(collection.items).toHaveLength(2);
expect(collection.items[0].name).toBe('Valid Request 1');
expect(collection.items[1].name).toBe('Valid Request 2');
expect(issues).toHaveLength(1);
expect(issues[0]).toMatchObject({
path: 'Bad Request',
severity: 'error',
message: 'Missing or invalid request method'
});
});
it('should import valid items and record errors for items that throw', async () => {
// Create a request with a value that will cause ensureString to throw (circular ref)
const circular = {};
circular.self = circular;
const items = [
makeRequest('Valid Request'),
{
name: 'Circular Request',
request: {
method: 'POST',
header: [{ key: circular, value: 'test' }],
url: { raw: 'https://example.com' }
}
}
];
const { collection, issues } = await postmanToBruno(makeCollection(items));
expect(collection.items).toHaveLength(1);
expect(collection.items[0].name).toBe('Valid Request');
expect(issues).toHaveLength(1);
expect(issues[0].path).toBe('Circular Request');
expect(issues[0].severity).toBe('error');
});
it('should record issues for nested folder items with full path', async () => {
const items = [
{
name: 'My Folder',
item: [
{
name: 'Subfolder',
item: [
{ name: 'Bad Nested', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
makeRequest('Good Nested')
]
}
]
}
];
const { collection, issues } = await postmanToBruno(makeCollection(items));
expect(collection.items).toHaveLength(1);
expect(collection.items[0].type).toBe('folder');
expect(collection.items[0].items[0].type).toBe('folder');
expect(collection.items[0].items[0].items).toHaveLength(1);
expect(collection.items[0].items[0].items[0].name).toBe('Good Nested');
expect(issues).toHaveLength(1);
expect(issues[0].path).toBe('My Folder / Subfolder / Bad Nested');
expect(issues[0].severity).toBe('error');
});
it('should return empty issues array for valid collections', async () => {
const items = [
makeRequest('Request 1'),
makeRequest('Request 2')
];
const { collection, issues } = await postmanToBruno(makeCollection(items));
expect(collection.items).toHaveLength(2);
expect(issues).toEqual([]);
});
it('should handle empty collections with no issues', async () => {
const { collection, issues } = await postmanToBruno(makeCollection([]));
expect(collection.items).toEqual([]);
expect(issues).toEqual([]);
});
it('should handle all items being malformed without throwing', async () => {
const items = [
{ name: 'Bad 1', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
{ name: 'Bad 2', request: { method: undefined, header: [], url: { raw: 'https://example.com' } } }
];
const { collection, issues } = await postmanToBruno(makeCollection(items));
expect(collection.items).toEqual([]);
expect(issues).toHaveLength(2);
expect(issues.every((i) => i.severity === 'error')).toBe(true);
});
it('should record warning for malformed collection-level variables', async () => {
const collectionData = makeCollection([makeRequest('Valid')], {
variable: 'not-an-array'
});
const { collection, issues } = await postmanToBruno(collectionData);
expect(collection.items).toHaveLength(1);
const warnings = issues.filter((i) => i.severity === 'warning');
expect(warnings).toHaveLength(1);
expect(warnings[0].path).toBe('Collection Variables');
});
it('should handle mixed valid/invalid items with folders and bad variables', async () => {
const items = [
makeRequest('r1', 'POST'),
{ name: 'r2-missing-method', request: { header: [], url: { raw: 'https://example.com' } } },
makeRequest('r3', 'POST'),
{
name: 'API Folder',
item: [
makeRequest('valid-in-folder', 'GET'),
{ name: 'r4-null-method', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
{
name: 'Nested Subfolder',
item: [
{ name: 'r5-empty-method', request: { method: '', header: [], url: { raw: 'https://example.com' } } },
makeRequest('valid-nested', 'DELETE')
]
}
]
}
];
const { collection, issues } = await postmanToBruno(makeCollection(items, { variable: 'not-an-array' }));
// 4 valid items: r1, r3, valid-in-folder, valid-nested
expect(collection.items).toHaveLength(3); // r1, r3, API Folder
expect(collection.items[0].name).toBe('r1');
expect(collection.items[1].name).toBe('r3');
expect(collection.items[2].name).toBe('API Folder');
expect(collection.items[2].items).toHaveLength(2); // valid-in-folder, Nested Subfolder
expect(collection.items[2].items[0].name).toBe('valid-in-folder');
expect(collection.items[2].items[1].name).toBe('Nested Subfolder');
expect(collection.items[2].items[1].items).toHaveLength(1); // valid-nested
expect(collection.items[2].items[1].items[0].name).toBe('valid-nested');
// 4 issues: 1 warning (variables) + 3 errors (missing/null/empty method)
expect(issues).toHaveLength(4);
expect(issues.filter((i) => i.severity === 'warning')).toHaveLength(1);
expect(issues.filter((i) => i.severity === 'error')).toHaveLength(3);
expect(issues.find((i) => i.path === 'r2-missing-method')).toBeTruthy();
expect(issues.find((i) => i.path === 'API Folder / r4-null-method')).toBeTruthy();
expect(issues.find((i) => i.path === 'API Folder / Nested Subfolder / r5-empty-method')).toBeTruthy();
});
it('should use fallback name for items without a name', async () => {
const items = [
{ request: { method: null, header: [], url: { raw: 'https://example.com' } } }
];
const { issues } = await postmanToBruno(makeCollection(items));
expect(issues).toHaveLength(1);
expect(issues[0].path).toBe('Item 1');
});
});

View File

@@ -4,7 +4,7 @@ import { invalidVariableCharacterRegex } from '../../../src/constants';
describe('postman-collection', () => {
it('should correctly import a valid Postman collection file', async () => {
const brunoCollection = await postmanToBruno(postmanCollection);
const { collection: brunoCollection } = await postmanToBruno(postmanCollection);
expect(brunoCollection).toMatchObject(expectedOutput);
});
@@ -55,7 +55,7 @@ describe('postman-collection', () => {
item: []
};
const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.root.request.vars.req).toEqual([
{
@@ -125,7 +125,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api/v1/resource'
@@ -178,7 +178,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api/v1/resource/'
@@ -231,7 +231,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api//resource'
@@ -266,7 +266,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNonStringVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNonStringVars);
const vars = brunoCollection.root.request.vars.req;
expect(vars).toHaveLength(3);
@@ -286,7 +286,7 @@ describe('postman-collection', () => {
item: []
};
const brunoCollection = await postmanToBruno(collectionWithEmptyVars);
const { collection: brunoCollection } = await postmanToBruno(collectionWithEmptyVars);
expect(brunoCollection.root.request.vars.req).toEqual([]);
});
@@ -348,7 +348,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithSettings);
const { collection: brunoCollection } = await postmanToBruno(collectionWithSettings);
// Test request with all settings
const requestWithAllSettings = brunoCollection.items[0];
@@ -402,7 +402,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithUndefinedAuthType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithUndefinedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -459,7 +459,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNullAuthType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNullAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -505,7 +505,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithUnexpectedAuthType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithUnexpectedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -562,7 +562,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithRequestUndefinedAuthType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithRequestUndefinedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -624,7 +624,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithFolderUnexpectedAuthType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFolderUnexpectedAuthType);
// Folder auth should default to 'none'
expect(brunoCollection.items[0].root.request.auth).toEqual({
@@ -675,7 +675,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNullHeaders);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNullHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(3);
@@ -715,7 +715,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNullUrlencoded);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNullUrlencoded);
const formUrlEncoded = brunoCollection.items[0].request.body.formUrlEncoded;
expect(formUrlEncoded).toHaveLength(3);
@@ -754,7 +754,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNullFormdata);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNullFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(2);
@@ -794,7 +794,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNullQueryParams);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNullQueryParams);
const params = brunoCollection.items[0].request.params;
// Fully-null entry should be skipped
@@ -871,7 +871,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNumericValues);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericValues);
const item = brunoCollection.items[0];
// Headers should have string values
@@ -950,7 +950,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNumericExamples);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericExamples);
const example = brunoCollection.items[0].examples[0];
// Example request headers
@@ -1011,7 +1011,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNumericAuth);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericAuth);
// Bearer token should be stringified
expect(brunoCollection.items[0].request.auth.mode).toBe('bearer');
@@ -1049,7 +1049,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithObjectAuth);
const { collection: brunoCollection } = await postmanToBruno(collectionWithObjectAuth);
expect(brunoCollection.items[0].request.auth.mode).toBe('basic');
expect(brunoCollection.items[0].request.auth.basic.username).toBe('12345');
@@ -1079,7 +1079,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithStringHeaders);
const { collection: brunoCollection } = await postmanToBruno(collectionWithStringHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(3);
@@ -1110,7 +1110,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithConcatenatedHeaders);
const { collection: brunoCollection } = await postmanToBruno(collectionWithConcatenatedHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(2);
@@ -1125,7 +1125,7 @@ describe('postman-collection', () => {
collection: { ...postmanCollection }
};
const brunoCollection = await postmanToBruno(wrappedCollection);
const { collection: brunoCollection } = await postmanToBruno(wrappedCollection);
expect(brunoCollection).toMatchObject(expectedOutput);
});
@@ -1148,7 +1148,7 @@ describe('postman-collection', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithNoValueHeader);
const { collection: brunoCollection } = await postmanToBruno(collectionWithNoValueHeader);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(1);
@@ -1247,7 +1247,7 @@ describe('postman-collection formdata import', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithFileFormdata);
const { collection: brunoCollection } = await postmanToBruno(collectionWithFileFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1287,7 +1287,7 @@ describe('postman-collection formdata import', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndSrc);
const { collection: brunoCollection } = await postmanToBruno(collectionWithDefaultTypeAndSrc);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1327,7 +1327,7 @@ describe('postman-collection formdata import', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndValueArray);
const { collection: brunoCollection } = await postmanToBruno(collectionWithDefaultTypeAndValueArray);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1368,7 +1368,7 @@ describe('postman-collection formdata import', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithContentType);
const { collection: brunoCollection } = await postmanToBruno(collectionWithContentType);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1412,7 +1412,7 @@ describe('postman-collection formdata import', () => {
]
};
const brunoCollection = await postmanToBruno(collectionWithMixedFormdata);
const { collection: brunoCollection } = await postmanToBruno(collectionWithMixedFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(2);

View File

@@ -26,7 +26,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth).toEqual({
mode: 'basic',
@@ -69,7 +69,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'inherit',
@@ -110,7 +110,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'inherit',
@@ -156,7 +156,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// Check folder first
expect(result.items[0].root.request.auth).toEqual({
@@ -199,7 +199,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'none', // <<<< KEY CHECK
@@ -250,7 +250,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -311,7 +311,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -366,7 +366,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth).toEqual({
mode: 'oauth1',
@@ -428,7 +428,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('oauth1');
expect(result.items[0].request.auth.oauth1.placement).toBe('header');
@@ -471,7 +471,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('oauth1');
expect(result.items[0].request.auth.oauth1.signatureMethod).toBe('RSA-SHA1');
@@ -508,7 +508,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// Collection root should have oauth1
expect(result.root.request.auth.mode).toBe('oauth1');
@@ -560,7 +560,7 @@ describe('Request Authentication', () => {
]
};
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -606,7 +606,7 @@ describe('Request Authentication', () => {
{ key: 'in', value: 'query' }
]);
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('apikey');
expect(result.items[0].request.auth.apikey).toEqual({
@@ -623,7 +623,7 @@ describe('Request Authentication', () => {
{ key: 'in', value: 'header' }
]);
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.apikey).toEqual({
key: 'X-API-Key',
@@ -638,7 +638,7 @@ describe('Request Authentication', () => {
{ key: 'value', value: 'secret-token' }
]);
const result = await postmanToBruno(postmanCollection);
const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.apikey.placement).toBe('header');
});

View File

@@ -12,7 +12,7 @@ describe('transformDescription function', () => {
item: []
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
});
@@ -26,7 +26,7 @@ describe('transformDescription function', () => {
item: []
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('This is a string description');
});
@@ -43,7 +43,7 @@ describe('transformDescription function', () => {
item: []
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('This is the content from the new Postman format');
});
@@ -59,7 +59,7 @@ describe('transformDescription function', () => {
item: []
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
});
@@ -84,7 +84,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.docs).toBe('This is a request description in new format');
});
@@ -106,7 +106,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].root.docs).toBe('This is a folder description in new format');
});
@@ -137,7 +137,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.headers[0].description).toBe('Authorization header description');
});
@@ -172,7 +172,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.params[0].description).toBe('Query parameter description');
});
@@ -207,7 +207,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.params[0].description).toBe('User ID path variable');
});
@@ -241,7 +241,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.body.multipartForm[0].description).toBe('Form field description');
});
@@ -275,7 +275,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.body.formUrlEncoded[0].description).toBe('URL encoded field description');
});
@@ -317,7 +317,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
// Collection description (string)
expect(brunoCollection.root.docs).toBe('Collection with string description');
@@ -357,7 +357,7 @@ describe('transformDescription function', () => {
]
};
const brunoCollection = await postmanToBruno(collection);
const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
expect(brunoCollection.items[0].request.docs).toBe('Description with special chars: !@#$%^&*()');
});

View File

@@ -2127,9 +2127,10 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => {
try {
// Convert Postman collection to Bruno format
const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true });
// Returns { collection, issues } where issues tracks items that were skipped or degraded
const result = await postmanToBruno(postmanCollection, { useWorkers: true });
return brunoCollection;
return result;
} catch (error) {
console.error('Error converting Postman to Bruno:', error);
return Promise.reject(error);

View File

@@ -0,0 +1,210 @@
{
"info": {
"_postman_id": "test-import-issues-001",
"name": "Import Issues Test Collection",
"description": "A Postman collection designed to test partial import handling. Contains a mix of valid requests, requests with missing/invalid methods, and edge cases that should be gracefully handled.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Valid GET Request",
"request": {
"method": "GET",
"header": [
{ "key": "Accept", "value": "application/json" }
],
"url": {
"raw": "https://api.example.com/users",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users"]
}
}
},
{
"name": "Missing Method (null)",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-1",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-1"]
}
}
},
{
"name": "Missing Method (absent)",
"request": {
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-2",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-2"]
}
}
},
{
"name": "Empty String Method",
"request": {
"method": "",
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-3",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-3"]
}
}
},
{
"name": "Whitespace-Only Method",
"request": {
"method": " ",
"header": [],
"url": {
"raw": "https://api.example.com/should-be-skipped-4",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["should-be-skipped-4"]
}
}
},
{
"name": "Valid POST Request",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\"name\": \"test\", \"email\": \"test@example.com\"}",
"options": {
"raw": { "language": "json" }
}
},
"url": {
"raw": "https://api.example.com/users",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users"]
}
}
},
{
"name": "API Folder",
"item": [
{
"name": "Valid Nested GET",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.example.com/nested/valid",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["nested", "valid"]
}
}
},
{
"name": "Nested Missing Method",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/nested/should-be-skipped",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["nested", "should-be-skipped"]
}
}
},
{
"name": "Deep Subfolder",
"item": [
{
"name": "Deep Valid Request",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "https://api.example.com/deep/valid",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["deep", "valid"]
}
}
},
{
"name": "Deep Bad Method",
"request": {
"method": null,
"header": [],
"url": {
"raw": "https://api.example.com/deep/should-be-skipped",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["deep", "should-be-skipped"]
}
}
}
]
}
]
},
{
"name": "Valid PUT Request",
"request": {
"method": "PUT",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{token}}" }
],
"body": {
"mode": "raw",
"raw": "{\"name\": \"updated\"}",
"options": {
"raw": { "language": "json" }
}
},
"url": {
"raw": "https://api.example.com/users/{{userId}}",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users", "{{userId}}"],
"variable": [
{ "key": "userId", "value": "123" }
]
}
}
},
{
"name": "No Request Object At All"
},
{
"name": "Valid PATCH Request",
"request": {
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"status\": \"active\"}"
},
"url": {
"raw": "https://api.example.com/users/1/status",
"protocol": "https",
"host": ["api", "example", "com"],
"path": ["users", "1", "status"]
}
}
}
],
"variable": [
{ "key": "baseUrl", "value": "https://api.example.com" },
{ "key": "token", "value": "test-token-123" }
]
}

View File

@@ -0,0 +1,70 @@
{
"info": {
"_postman_id": "test-many-import-issues-001",
"name": "Many Import Issues Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Valid GET Request",
"request": {
"method": "GET",
"header": [],
"url": { "raw": "https://api.example.com/users", "protocol": "https", "host": ["api", "example", "com"], "path": ["users"] }
}
},
{ "name": "Bad Method 1", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-1"] } } },
{ "name": "Bad Method 2", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-2"] } } },
{ "name": "Bad Method 3", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-3"] } } },
{ "name": "Bad Method 4", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-4"] } } },
{ "name": "Bad Method 5", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-5"] } } },
{ "name": "Bad Method 6", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-6", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-6"] } } },
{ "name": "Bad Method 7", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-7", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-7"] } } },
{ "name": "Bad Method 8", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-8", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-8"] } } },
{ "name": "Bad Method 9", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-9", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-9"] } } },
{ "name": "Bad Method 10", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-10", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-10"] } } },
{ "name": "Empty Method 1", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-1"] } } },
{ "name": "Empty Method 2", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-2"] } } },
{ "name": "Empty Method 3", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-3"] } } },
{ "name": "Empty Method 4", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-4"] } } },
{ "name": "Empty Method 5", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-5"] } } },
{
"name": "Valid POST Request",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": { "mode": "raw", "raw": "{\"name\": \"test\"}" },
"url": { "raw": "https://api.example.com/users", "protocol": "https", "host": ["api", "example", "com"], "path": ["users"] }
}
},
{ "name": "Whitespace Method 1", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-1"] } } },
{ "name": "Whitespace Method 2", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-2"] } } },
{ "name": "Whitespace Method 3", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-3"] } } },
{ "name": "Whitespace Method 4", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-4"] } } },
{ "name": "Whitespace Method 5", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-5"] } } },
{ "name": "Missing Method 1", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-1"] } } },
{ "name": "Missing Method 2", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-2"] } } },
{ "name": "Missing Method 3", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-3"] } } },
{ "name": "Missing Method 4", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-4"] } } },
{ "name": "Missing Method 5", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-5"] } } },
{
"name": "API Folder",
"item": [
{ "name": "Nested Bad 1", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-1"] } } },
{ "name": "Nested Bad 2", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-2"] } } },
{ "name": "Nested Bad 3", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-3"] } } },
{ "name": "Nested Bad 4", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-4"] } } },
{ "name": "Nested Bad 5", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-5"] } } }
]
},
{
"name": "Valid PUT Request",
"request": {
"method": "PUT",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": { "mode": "raw", "raw": "{\"name\": \"updated\"}" },
"url": { "raw": "https://api.example.com/users/1", "protocol": "https", "host": ["api", "example", "com"], "path": ["users", "1"] }
}
}
]
}

View File

@@ -0,0 +1,70 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { closeAllCollections, dismissImportIssuesToasts, importCollection } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
test.describe('Import Postman Collection with many issues (URL too long warning)', () => {
test.afterEach(async ({ page }) => {
await dismissImportIssuesToasts(page);
await closeAllCollections(page);
});
test('should show URL-too-long warning when include failed request data is checked', async ({ page, createTmpDir }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-many-import-issues.json');
const tmpDir = await createTmpDir('postman-many-issues');
const locators = buildCommonLocators(page);
await importCollection(page, postmanFile, tmpDir, {
expectedCollectionName: 'Many Import Issues Collection',
expectIssues: true
});
await test.step('Verify toast title and action buttons are visible', async () => {
await expect(locators.import.issuesToastTitle()).toBeVisible();
await expect(locators.import.issuesToastTitle()).toContainText('item(s) skipped');
await expect(locators.import.issuesToastCopyBtn()).toBeVisible();
await expect(locators.import.issuesToastReportBtn()).toBeVisible();
});
await test.step('Check include failed request data checkbox', async () => {
const checkbox = locators.import.issuesToastIncludeItemsCheckbox();
await expect(checkbox).toBeVisible();
await checkbox.check();
await expect(checkbox).toBeChecked();
});
await test.step('Verify URL-too-long warning appears after checking include items', async () => {
const warning = locators.import.issuesToastUrlTooLongWarning();
await expect(warning).toBeVisible();
await expect(warning).toContainText('clipboard');
});
await test.step('Verify valid requests were imported', async () => {
await expect(locators.sidebar.request('Valid GET Request')).toBeVisible();
await expect(locators.sidebar.request('Valid POST Request')).toBeVisible();
await expect(locators.sidebar.request('Valid PUT Request')).toBeVisible();
});
await test.step('Report on GitHub copies to clipboard and opens URL without body', async () => {
await page.evaluate(() => {
(window as any).__capturedOpenUrl = null;
window.open = (url?: string | URL) => {
(window as any).__capturedOpenUrl = url != null ? String(url) : '';
return null;
};
});
await locators.import.issuesToastReportBtn().click();
// Should show clipboard success toast
await expect(page.getByText('Issue details copied')).toBeVisible({ timeout: 3000 });
// URL should have title but NOT body (since it was too long)
const openedUrl = await page.evaluate(() => (window as any).__capturedOpenUrl as string);
expect(openedUrl).toContain('https://github.com/usebruno/bruno/issues/new');
expect(openedUrl).toContain('title=');
expect(openedUrl).toContain('labels=bug');
expect(openedUrl).not.toContain('Missing+or+invalid+request+method');
});
});
});

View File

@@ -0,0 +1,118 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { closeAllCollections, dismissImportIssuesToasts, importCollection } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
test.describe('Import Postman Collection with partial import issues', () => {
test.afterEach(async ({ page }) => {
await dismissImportIssuesToasts(page);
await closeAllCollections(page);
});
test('should import valid requests and show issues toast for skipped items', async ({ page, createTmpDir }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
const tmpDir = await createTmpDir('postman-partial-import');
const locators = buildCommonLocators(page);
await importCollection(page, postmanFile, tmpDir, {
expectedCollectionName: 'Import Issues Test Collection',
expectIssues: true
});
await test.step('Verify import issues toast content', async () => {
const toastTitle = locators.import.issuesToastTitle();
await expect(toastTitle).toBeVisible();
await expect(toastTitle).toContainText('item(s) skipped');
});
await test.step('Verify toast action buttons are visible', async () => {
await expect(locators.import.issuesToastCopyBtn()).toBeVisible();
await expect(locators.import.issuesToastReportBtn()).toBeVisible();
});
await test.step('Verify include items checkbox is visible', async () => {
await expect(locators.import.issuesToastIncludeItemsCheckbox()).toBeVisible();
});
await test.step('Verify valid top-level requests were imported', async () => {
await expect(locators.sidebar.request('Valid GET Request')).toBeVisible();
await expect(locators.sidebar.request('Valid POST Request')).toBeVisible();
await expect(locators.sidebar.request('Valid PUT Request')).toBeVisible();
await expect(locators.sidebar.request('Valid PATCH Request')).toBeVisible();
});
await test.step('Verify skipped requests are NOT in the sidebar', async () => {
await expect(locators.sidebar.request('Missing Method (null)')).not.toBeVisible();
await expect(locators.sidebar.request('Missing Method (absent)')).not.toBeVisible();
await expect(locators.sidebar.request('Empty String Method')).not.toBeVisible();
await expect(locators.sidebar.request('Whitespace-Only Method')).not.toBeVisible();
});
await test.step('Verify folder and nested valid requests were imported', async () => {
const folder = locators.sidebar.folder('API Folder');
await expect(folder).toBeVisible();
await folder.click();
await expect(locators.sidebar.request('Valid Nested GET')).toBeVisible();
const subfolder = locators.sidebar.folder('Deep Subfolder');
await expect(subfolder).toBeVisible();
await subfolder.click();
await expect(locators.sidebar.request('Deep Valid Request')).toBeVisible();
});
await test.step('Verify nested skipped requests are NOT in the sidebar', async () => {
await expect(locators.sidebar.request('Nested Missing Method')).not.toBeVisible();
await expect(locators.sidebar.request('Deep Bad Method')).not.toBeVisible();
});
});
test('should allow copying import issues to clipboard', async ({ page, createTmpDir }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
const tmpDir = await createTmpDir('postman-partial-import-copy');
const locators = buildCommonLocators(page);
await importCollection(page, postmanFile, tmpDir, {
expectedCollectionName: 'Import Issues Test Collection',
expectIssues: true
});
await test.step('Click copy button and verify success toast', async () => {
await locators.import.issuesToastCopyBtn().click();
await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 3000 });
});
});
test('should open GitHub issue with prefilled details when clicking Report on GitHub', async ({ page, createTmpDir }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
const tmpDir = await createTmpDir('postman-partial-import-report');
const locators = buildCommonLocators(page);
await importCollection(page, postmanFile, tmpDir, {
expectedCollectionName: 'Import Issues Test Collection',
expectIssues: true
});
await test.step('Mock window.open and click Report on GitHub', async () => {
// Mock window.open to capture the URL instead of opening a browser
await page.evaluate(() => {
(window as any).__capturedOpenUrl = null;
window.open = (url?: string | URL) => {
(window as any).__capturedOpenUrl = url != null ? String(url) : '';
return null;
};
});
await locators.import.issuesToastReportBtn().click();
const openedUrl = await page.evaluate(() => (window as any).__capturedOpenUrl as string);
expect(openedUrl).toContain('https://github.com/usebruno/bruno/issues/new');
expect(openedUrl).toContain('title=');
expect(openedUrl).toContain('Postman+import');
expect(openedUrl).toContain('labels=bug');
expect(openedUrl).toContain('Missing+or+invalid+request+method');
});
});
});

View File

@@ -18,6 +18,22 @@ const waitForReadyPage = (
options: WaitForAppReadyOptions = {}
) => waitForReadyPageImpl(app, options);
/**
* Dismiss all import issues toasts (they use infinite duration and persist across tests).
* @param page - The page object
* @returns void
*/
const dismissImportIssuesToasts = async (page: Page) => {
await test.step('Dismiss import issues toasts', async () => {
const toasts = page.getByTestId('import-issues-toast');
while (await toasts.count() > 0) {
const toast = toasts.first();
await toast.getByTestId('import-issues-toast-close').click();
await expect(toast).not.toBeVisible({ timeout: 5000 });
}
});
};
/**
* Close all collections
* @param page - The page object
@@ -428,6 +444,7 @@ const deleteCollectionFromOverview = async (page: Page, collectionName: string)
*/
type ImportCollectionOptions = {
expectedCollectionName?: string;
expectIssues?: boolean;
};
const importCollection = async (
@@ -471,6 +488,11 @@ const importCollection = async (
).toBeVisible();
}
// Wait for import issues toast if expected
if (options.expectIssues) {
await expect(locators.import.issuesToast()).toBeVisible({ timeout: 10000 });
}
if (options.expectedCollectionName) {
await openCollection(page, options.expectedCollectionName);
}
@@ -1546,6 +1568,7 @@ const openWorkspaceFromDialog = async (app: any, page: any, targetPath: string)
export {
waitForReadyPage,
dismissImportIssuesToasts,
closeAllCollections,
openCollection,
createCollection,

View File

@@ -129,7 +129,19 @@ export const buildCommonLocators = (page: Page) => ({
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }),
parsingError: () => page.getByTestId('import-error-message'),
browseLink: (root?: Locator) => (root ?? page).getByTestId('import-collection-browse-link'),
importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn')
importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn'),
...(() => {
const issuesToast = () => page.getByTestId('import-issues-toast').last();
return {
issuesToast,
issuesToastTitle: () => issuesToast().getByTestId('import-issues-toast-title'),
issuesToastCopyBtn: () => issuesToast().getByTestId('import-issues-copy-btn'),
issuesToastReportBtn: () => issuesToast().getByTestId('import-issues-report-btn'),
issuesToastIncludeItemsCheckbox: () => issuesToast().getByTestId('import-issues-include-items-checkbox'),
issuesToastCloseBtn: () => issuesToast().getByTestId('import-issues-toast-close'),
issuesToastUrlTooLongWarning: () => issuesToast().getByTestId('import-issues-url-too-long-warning')
};
})()
},
/**
* Build generic table locators for any table with a testId