mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 14:08:38 +00:00
fix(bru-1142): collection and global environments import and export functionality updates (#5910)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
/* Environment item styling */
|
||||
.environment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
.environment-name {
|
||||
color: ${(props) => props.theme.text};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,269 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal';
|
||||
import { exportBrunoEnvironment } from 'utils/exporters/bruno-environment';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Helper function to truncate environment names
|
||||
const truncateEnvName = (name) => {
|
||||
if (name.length > 40) {
|
||||
return name.substring(0, 40) + '...';
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [filePath, setFilePath] = useState('');
|
||||
const [selectedEnvironments, setSelectedEnvironments] = useState({});
|
||||
const [exportFormat, setExportFormat] = useState(environments.length > 1 ? 'single-file' : 'single-object');
|
||||
|
||||
// Initialize selected environments
|
||||
useEffect(() => {
|
||||
const initialSelection = {};
|
||||
|
||||
// Add all environments and select them by default
|
||||
environments.forEach((env) => {
|
||||
initialSelection[env.uid] = true;
|
||||
});
|
||||
|
||||
setSelectedEnvironments(initialSelection);
|
||||
}, [environments]);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedCount = Object.values(selectedEnvironments).filter(Boolean).length;
|
||||
if (selectedCount <= 1) {
|
||||
setExportFormat('single-object');
|
||||
}
|
||||
if (exportFormat === 'single-object' && selectedCount > 1) {
|
||||
setExportFormat('single-file');
|
||||
}
|
||||
}, [selectedEnvironments]);
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
if (typeof dirPath === 'string') {
|
||||
setFilePath(dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setFilePath('');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnvironmentToggle = (envUid) => {
|
||||
setSelectedEnvironments((prev) => {
|
||||
const newSelection = {
|
||||
...prev,
|
||||
[envUid]: !prev[envUid]
|
||||
};
|
||||
return newSelection;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const allSelected = environments.every((env) => selectedEnvironments[env.uid]) || false;
|
||||
|
||||
const newSelection = environments.reduce((acc, env) => ({
|
||||
...acc,
|
||||
[env.uid]: !allSelected
|
||||
}), {}) || {};
|
||||
|
||||
setSelectedEnvironments(newSelection);
|
||||
};
|
||||
|
||||
// Memoized selected environments and count
|
||||
const selectedEnvs = useMemo(() => {
|
||||
return environments.filter((env) => selectedEnvironments[env.uid]) || [];
|
||||
}, [environments, selectedEnvironments]);
|
||||
|
||||
const selectedCount = selectedEnvs.length;
|
||||
|
||||
const exportFormatOptions = useMemo(() => {
|
||||
const isMultiple = selectedCount > 1;
|
||||
|
||||
if (isMultiple) {
|
||||
return [
|
||||
{ value: 'single-file', label: 'Single JSON file', description: 'All environments in one JSON array' },
|
||||
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: false }
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ value: 'single-object', label: 'Single JSON file', description: 'Export as a single environment JSON object' },
|
||||
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: true }
|
||||
];
|
||||
}, [selectedCount, exportFormat]);
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
if (!filePath) {
|
||||
toast.error('Please select a location to save the files');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCount === 0) {
|
||||
toast.error('Please select at least one environment to export');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportBrunoEnvironment({ environments: selectedEnvs, environmentType, filePath, exportFormat });
|
||||
|
||||
const successMessage = exportFormat === 'folder'
|
||||
? `Environments exported successfully to bruno-${environmentType}-environments folder`
|
||||
: 'Environment(s) exported successfully';
|
||||
toast.success(successMessage);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
toast.error(error.message || 'Failed to export environments');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Export Environments"
|
||||
hideFooter={true}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<div className="py-2">
|
||||
{/* Environments Section */}
|
||||
<div className="mb-4">
|
||||
{environments && environments.length > 0 ? (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-2 pb-1">
|
||||
<h3 className="font-semibold text-sm text-theme">
|
||||
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAll}
|
||||
className="text-xs text-link px-1 py-0.5 rounded transition-colors"
|
||||
>
|
||||
{environments.every((env) => selectedEnvironments[env.uid]) ? 'Deselect All' : 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 flex-1 overflow-y-auto">
|
||||
{environments.map((env) => (
|
||||
<label key={env.uid} className="environment-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedEnvironments[env.uid] || false}
|
||||
onChange={() => handleEnvironmentToggle(env.uid)}
|
||||
disabled={isExporting}
|
||||
className="w-3.5 h-3.5 flex-shrink-0"
|
||||
/>
|
||||
<span className="environment-name">{truncateEnvName(env.name)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-2 pb-1">
|
||||
<h3 className="font-semibold text-sm text-theme">
|
||||
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-center flex-1 p-4 text-center">
|
||||
<span className="text-xs text-muted">
|
||||
No {environmentType === 'global' ? 'global' : 'collection'} environments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Export Format Section */}
|
||||
{selectedCount > 0 && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2 text-theme">
|
||||
Export Format
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{exportFormatOptions.map((option) => (
|
||||
<label key={option.value} className={`flex items-start p-2 rounded transition-colors ${option.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="exportFormat"
|
||||
value={option.value}
|
||||
checked={exportFormat === option.value}
|
||||
onChange={(e) => setExportFormat(e.target.value)}
|
||||
disabled={isExporting || option.disabled}
|
||||
className={`mt-0.5 mr-3 w-4 h-4 ${option.disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
/>
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${option.disabled ? 'text-muted' : 'text-theme'}`}>{option.label}</div>
|
||||
<div className="text-xs text-muted">{option.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location Input Section */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="export-location" className="block text-sm font-medium mb-2 text-theme">
|
||||
Location
|
||||
</label>
|
||||
<div className="flex flex-col relative items-center">
|
||||
<input
|
||||
id="export-location"
|
||||
type="text"
|
||||
className={`flex-1 textbox w-full ${isExporting || selectedCount <= 0 ? '' : 'cursor-pointer'}`}
|
||||
title={filePath}
|
||||
value={filePath}
|
||||
onClick={browse}
|
||||
onChange={(e) => setFilePath(e.target.value)}
|
||||
disabled={isExporting || selectedCount <= 0}
|
||||
placeholder="Select a target location"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Actions */}
|
||||
<div className="flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-cancel mt-2 flex items-center"
|
||||
onClick={onClose}
|
||||
disabled={isExporting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary mt-2 flex items-center"
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || selectedCount === 0}
|
||||
>
|
||||
{isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportEnvironmentModal;
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import importBrunoEnvironment from 'utils/importers/bruno-environment';
|
||||
import { readMultipleFiles } from 'utils/importers/file-reader';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { IconFileImport } from '@tabler/icons';
|
||||
|
||||
const ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEnvironmentCreated }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const isGlobal = type === 'global';
|
||||
|
||||
// Validate required props
|
||||
if (!isGlobal && !collection) {
|
||||
console.error('ImportEnvironmentModal: collection prop is required when type is "collection"');
|
||||
return null;
|
||||
}
|
||||
const modalTitle = isGlobal ? 'Import Global Environment' : 'Import Environment';
|
||||
const modalTestId = isGlobal ? 'import-global-environment-modal' : 'import-environment-modal';
|
||||
const importTestId = isGlobal ? 'import-global-environment' : 'import-environment';
|
||||
|
||||
const processEnvironments = async (environments, successMessage) => {
|
||||
const validEnvironments = environments.filter((env) => {
|
||||
if (env.name && env.name !== 'undefined') {
|
||||
return true;
|
||||
} else {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (validEnvironments.length === 0) {
|
||||
toast.error('No valid environments found to import');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Process environments sequentially to ensure unique name checking considers previously imported environments
|
||||
let importedCount = 0;
|
||||
for (const environment of validEnvironments) {
|
||||
const action = isGlobal
|
||||
? addGlobalEnvironment({ name: environment.name, variables: environment.variables })
|
||||
: importEnvironment({ name: environment.name, variables: environment.variables, collectionUid: collection?.uid });
|
||||
|
||||
await dispatch(action);
|
||||
importedCount++;
|
||||
}
|
||||
|
||||
toast.success(`${importedCount > 1 ? `${importedCount} environments` : 'Environment'} imported successfully`);
|
||||
} catch (error) {
|
||||
toast.error('An error occurred while importing the environment(s)');
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const detectEnvironmentFormat = (data) => {
|
||||
// bruno environment `single-object` export type
|
||||
if (data.info && data.info.type === 'bruno-environment') {
|
||||
return 'bruno';
|
||||
} else if (Array.isArray(data)) {
|
||||
// bruno environment`single-file` export type
|
||||
return data.some((env) => env.info && env.info.type === 'bruno-environment') ? 'bruno' : 'postman';
|
||||
} else if (data.id && data.values) {
|
||||
// postman environment
|
||||
return 'postman';
|
||||
}
|
||||
return 'bruno';
|
||||
};
|
||||
|
||||
const handleImportEnvironment = async (files) => {
|
||||
try {
|
||||
// Read and parse all files
|
||||
const parsedFiles = await readMultipleFiles(Array.from(files));
|
||||
|
||||
// Detect format from first file's content
|
||||
const format = detectEnvironmentFormat(parsedFiles[0].content);
|
||||
let environments;
|
||||
|
||||
if (format === 'postman') {
|
||||
environments = await importPostmanEnvironment(parsedFiles);
|
||||
} else {
|
||||
environments = await importBrunoEnvironment(parsedFiles);
|
||||
}
|
||||
|
||||
await processEnvironments(environments);
|
||||
onClose();
|
||||
if (onEnvironmentCreated) {
|
||||
onEnvironmentCreated();
|
||||
}
|
||||
} catch (err) {
|
||||
toastError(err, 'Import environment failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.accept = '.json';
|
||||
input.onchange = (e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
handleImportEnvironment(e.target.files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
handleImportEnvironment(files);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="md" title={modalTitle} hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId={modalTestId}>
|
||||
<div className="py-2">
|
||||
<div
|
||||
className={`flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed p-12 text-center cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 ${
|
||||
isDragOver
|
||||
? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-zinc-300 dark:border-zinc-400 hover:border-zinc-400'
|
||||
}`}
|
||||
onClick={handleFileSelect}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
data-testid={importTestId}
|
||||
>
|
||||
<IconFileImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">
|
||||
{isDragOver ? 'Drop your environment files here' : 'Import your environments'}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-muted">
|
||||
Drag & drop JSON files/folders or click to browse. Supports both Bruno and Postman formats.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironmentModal;
|
||||
@@ -11,9 +11,8 @@ import EnvironmentListContent from './EnvironmentListContent/index';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings';
|
||||
import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
|
||||
import ImportEnvironment from '../EnvironmentSettings/ImportEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
|
||||
import ImportGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -242,7 +241,8 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
)}
|
||||
|
||||
{showImportGlobalModal && (
|
||||
<ImportGlobalEnvironment
|
||||
<ImportEnvironmentModal
|
||||
type="global"
|
||||
onClose={() => setShowImportGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowGlobalSettings(true);
|
||||
@@ -261,7 +261,8 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
)}
|
||||
|
||||
{showImportCollectionModal && (
|
||||
<ImportEnvironment
|
||||
<ImportEnvironmentModal
|
||||
type="collection"
|
||||
collection={collection}
|
||||
onClose={() => setShowImportCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
@@ -30,10 +31,22 @@ const EnvironmentDetails = ({ environment, collection, setIsModified, onClose })
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-semibold break-all">{environment.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-x-4 pl-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
<div className="flex gap-x-2 pl-2">
|
||||
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
|
||||
<IconTrash
|
||||
className="cursor-pointer"
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => setOpenDeleteModal(true)}
|
||||
data-testid="delete-environment-button"
|
||||
/>
|
||||
</ToolHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock } from '@tabler/icons';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => {
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose, setShowExportModal }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
@@ -96,7 +96,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
{openImportModal && <ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
|
||||
<div className="flex">
|
||||
@@ -129,6 +129,10 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => setShowExportModal(true)}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Export</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { IconDatabaseImport } from '@tabler/icons';
|
||||
|
||||
const ImportEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environments) => {
|
||||
environments
|
||||
.filter((env) =>
|
||||
env.name && env.name !== 'undefined'
|
||||
? true
|
||||
: () => {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
)
|
||||
.map((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onClose();
|
||||
// Call the callback if provided
|
||||
if (onEnvironmentCreated) {
|
||||
onEnvironmentCreated();
|
||||
}
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-environment-modal">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImportPostmanEnvironment}
|
||||
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
||||
data-testid="import-postman-environment"
|
||||
>
|
||||
<IconDatabaseImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||
</button>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironment;
|
||||
@@ -3,8 +3,9 @@ import React, { useState } from 'react';
|
||||
import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironment from './ImportEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
@@ -47,6 +48,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const { environments } = collection;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [tab, setTab] = useState('default');
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
@@ -54,7 +56,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<DefaultTab setTab={setTab} />
|
||||
)}
|
||||
@@ -64,16 +66,26 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
collection={collection}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
collection={collection}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onClose={onClose}
|
||||
setShowExportModal={setShowExportModal}
|
||||
/>
|
||||
</Modal>
|
||||
{showExportModal && (
|
||||
<ExportEnvironmentModal
|
||||
onClose={() => setShowExportModal(false)}
|
||||
environments={collection.environments}
|
||||
environmentType="collection"
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
|
||||
const EnvironmentDetails = ({ environment, setIsModified, collection, allEnvironments }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
@@ -29,15 +30,26 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-semibold break-all">{environment.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-x-4 pl-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
<div className="flex gap-x-2 pl-2">
|
||||
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} data-testid="delete-environment-button" />
|
||||
</ToolHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables environment={environment} setIsModified={setIsModified} collection={collection} />
|
||||
<EnvironmentVariables
|
||||
environment={environment}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
allEnvironments={allEnvironments}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,15 +2,15 @@ import React, { useEffect, useState } from 'react';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock } from '@tabler/icons';
|
||||
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { isEqual } from 'lodash';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection }) => {
|
||||
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection, setShowExportModal }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
|
||||
@@ -38,7 +38,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = environments?.find(env => env.uid === activeEnvironmentUid) || environments?.[0];
|
||||
const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0] || null;
|
||||
|
||||
setSelectedEnvironment(environment);
|
||||
setOriginalEnvironmentVariables(environment?.variables || []);
|
||||
@@ -90,6 +90,12 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
setOpenManageSecretsModal(true);
|
||||
};
|
||||
|
||||
const handleExportClick = () => {
|
||||
if (setShowExportModal) {
|
||||
setShowExportModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSwitch = (saveChanges) => {
|
||||
if (!saveChanges) {
|
||||
setSwitchEnvConfirmClose(false);
|
||||
@@ -99,7 +105,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment onClose={() => setOpenImportModal(false)} />}
|
||||
{openImportModal && <ImportEnvironmentModal type="global" onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
|
||||
<div className="flex">
|
||||
@@ -132,6 +138,10 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleExportClick()}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Export</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
@@ -144,6 +154,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
collection={collection}
|
||||
allEnvironments={environments}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { IconDatabaseImport } from '@tabler/icons';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { uuid } from 'utils/common/index';
|
||||
|
||||
const ImportEnvironment = ({ onClose, onEnvironmentCreated }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environments) => {
|
||||
environments
|
||||
.filter((env) =>
|
||||
env.name && env.name !== 'undefined'
|
||||
? true
|
||||
: () => {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
)
|
||||
.map((environment) => {
|
||||
dispatch(addGlobalEnvironment({ name: environment.name, variables: environment.variables }))
|
||||
.then(() => {
|
||||
toast.success('Global Environment imported successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onClose();
|
||||
// Call the callback if provided
|
||||
if (onEnvironmentCreated) {
|
||||
onEnvironmentCreated();
|
||||
}
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Global Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-global-environment-modal">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImportPostmanEnvironment}
|
||||
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
||||
data-testid="import-postman-global-environment"
|
||||
>
|
||||
<IconDatabaseImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||
</button>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironment;
|
||||
@@ -4,7 +4,8 @@ import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ImportEnvironment from './ImportEnvironment/index';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
@@ -44,6 +45,7 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
|
||||
const environments = globalEnvironments;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [tab, setTab] = useState('default');
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
@@ -51,7 +53,7 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironment onClose={() => setTab('default')} />
|
||||
<ImportEnvironmentModal type="global" onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<DefaultTab setTab={setTab} />
|
||||
)}
|
||||
@@ -61,17 +63,27 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
environments={globalEnvironments}
|
||||
activeEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
/>
|
||||
</Modal>
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
environments={globalEnvironments}
|
||||
activeEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
setShowExportModal={setShowExportModal}
|
||||
/>
|
||||
</Modal>
|
||||
{showExportModal && (
|
||||
<ExportEnvironmentModal
|
||||
onClose={() => setShowExportModal(false)}
|
||||
environments={globalEnvironments}
|
||||
environmentType="global"
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1339,7 +1339,7 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const importEnvironment = (name, variables, collectionUid) => (dispatch, getState) => {
|
||||
export const importEnvironment = ({ name, variables, collectionUid }) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -87,13 +87,17 @@ export const {
|
||||
_deleteGlobalEnvironment
|
||||
} = globalEnvironmentsSlice.actions;
|
||||
|
||||
export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => {
|
||||
export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uid = uuid();
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { name, uid, variables })
|
||||
.then(() => dispatch(_addGlobalEnvironment({ name, uid, variables })))
|
||||
.then((result) => {
|
||||
const finalName = result?.name || name;
|
||||
dispatch(_addGlobalEnvironment({ name: finalName, uid, variables }));
|
||||
})
|
||||
.then(() => dispatch(selectGlobalEnvironment({ environmentUid: uid })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
@@ -109,7 +113,11 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables })
|
||||
.then(() => dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables })))
|
||||
.then((result) => {
|
||||
// Use the unique name returned by the IPC handler
|
||||
const finalName = result?.name || name;
|
||||
dispatch(_copyGlobalEnvironment({ name: finalName, uid, variables: baseEnv.variables }));
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -29,3 +29,13 @@ export const buildPersistedEnvVariables = (variables, { mode, persistedNames } =
|
||||
// default to save mode
|
||||
return src.map(toPersistedEnvVarForSave);
|
||||
};
|
||||
|
||||
export const buildEnvVariable = (obj) => {
|
||||
return {
|
||||
name: obj.name ?? '',
|
||||
value: !!obj.secret ? '' : (obj.value ?? ''),
|
||||
type: 'text',
|
||||
enabled: obj.enabled !== false,
|
||||
secret: !!obj.secret
|
||||
};
|
||||
};
|
||||
|
||||
23
packages/bruno-app/src/utils/exporters/bruno-environment.js
Normal file
23
packages/bruno-app/src/utils/exporters/bruno-environment.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { buildEnvVariable } from 'utils/environments';
|
||||
|
||||
export const exportBrunoEnvironment = async ({ environments, environmentType, filePath, exportFormat = 'folder' }) => {
|
||||
try {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
let cleanEnvironments = environments.map((environment) => ({
|
||||
name: environment.name,
|
||||
variables: (environment.variables || []).map(buildEnvVariable)
|
||||
}));
|
||||
|
||||
await ipcRenderer.invoke('renderer:export-environment', {
|
||||
environments: cleanEnvironments,
|
||||
environmentType,
|
||||
format: 'json',
|
||||
filePath,
|
||||
exportFormat
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error exporting ${environmentType} environment as .json:`, error);
|
||||
throw new Error(`Failed to export ${environmentType} environments.`);
|
||||
}
|
||||
};
|
||||
90
packages/bruno-app/src/utils/importers/bruno-environment.js
Normal file
90
packages/bruno-app/src/utils/importers/bruno-environment.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { buildEnvVariable } from 'utils/environments';
|
||||
|
||||
const validateBrunoEnvironment = (env) => {
|
||||
if (!env || typeof env !== 'object') {
|
||||
throw new BrunoError('Invalid environment: expected an object');
|
||||
}
|
||||
|
||||
if (!Array.isArray(env.variables)) {
|
||||
throw new BrunoError('Invalid environment: missing or invalid variables array');
|
||||
}
|
||||
|
||||
// Validate each variable
|
||||
env.variables.forEach((variable, index) => {
|
||||
if (!variable || typeof variable !== 'object') {
|
||||
throw new BrunoError(`Invalid variable at index ${index}: expected an object`);
|
||||
}
|
||||
if (!variable.name || typeof variable.name !== 'string') {
|
||||
throw new BrunoError(`Invalid variable at index ${index}: missing or invalid name`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: env.name || 'Imported Environment',
|
||||
variables: env.variables.map(buildEnvVariable)
|
||||
};
|
||||
};
|
||||
|
||||
const processEnvironmentData = (data, fileName) => {
|
||||
try {
|
||||
// Handle new single-file format with environments array
|
||||
if (data.info && data.info.type === 'bruno-environment' && Array.isArray(data.environments)) {
|
||||
return data.environments.map((env, index) => {
|
||||
try {
|
||||
return validateBrunoEnvironment(env);
|
||||
} catch (err) {
|
||||
throw new BrunoError(`Error in environment ${index + 1} from ${fileName}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle array of environments (old format)
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((env, index) => {
|
||||
try {
|
||||
return validateBrunoEnvironment(env);
|
||||
} catch (err) {
|
||||
throw new BrunoError(`Error in environment ${index + 1} from ${fileName}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle single environment object
|
||||
return [validateBrunoEnvironment(data)];
|
||||
} catch (err) {
|
||||
throw new BrunoError(`Error processing ${fileName}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const processFiles = (parsedFiles) => {
|
||||
const allEnvironments = [];
|
||||
|
||||
for (const parsedFile of parsedFiles) {
|
||||
try {
|
||||
const environments = processEnvironmentData(parsedFile.content, parsedFile.fileName);
|
||||
allEnvironments.push(...environments);
|
||||
} catch (err) {
|
||||
throw new BrunoError(`Failed to process ${parsedFile.fileName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return allEnvironments;
|
||||
};
|
||||
|
||||
const importBrunoEnvironment = (parsedFiles) => {
|
||||
try {
|
||||
if (!parsedFiles || parsedFiles.length === 0) {
|
||||
throw new BrunoError('No files provided');
|
||||
}
|
||||
|
||||
const environments = processFiles(parsedFiles);
|
||||
return environments;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err instanceof BrunoError ? err : new BrunoError('Import Bruno environment failed');
|
||||
}
|
||||
};
|
||||
|
||||
export { importBrunoEnvironment, processEnvironmentData };
|
||||
export default importBrunoEnvironment;
|
||||
41
packages/bruno-app/src/utils/importers/file-reader.js
Normal file
41
packages/bruno-app/src/utils/importers/file-reader.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
const readFile = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.result);
|
||||
resolve({ fileName: file.name, content: parsed });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(new BrunoError(`Unable to parse JSON file: ${file.name}`));
|
||||
}
|
||||
};
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
export const readMultipleFiles = async (files) => {
|
||||
if (!files || files.length === 0) {
|
||||
throw new BrunoError('No files selected');
|
||||
}
|
||||
|
||||
const parsedFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.name.toLowerCase().endsWith('.json')) {
|
||||
throw new BrunoError(`Invalid file type: ${file.name}. Only JSON files are supported.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedFile = await readFile(file);
|
||||
parsedFiles.push(parsedFile);
|
||||
} catch (err) {
|
||||
throw new BrunoError(`Failed to read ${file.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedFiles;
|
||||
};
|
||||
@@ -1,45 +1,25 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { postmanToBrunoEnvironment } from '@usebruno/converters';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
const importEnvironment = async (parsedFiles) => {
|
||||
try {
|
||||
const environments = [];
|
||||
|
||||
for (const parsedFile of parsedFiles) {
|
||||
try {
|
||||
let parsedPostmanEnvironment = JSON.parse(e.target.result);
|
||||
resolve(parsedPostmanEnvironment);
|
||||
const environment = postmanToBrunoEnvironment(parsedFile.content);
|
||||
environments.push(environment);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(new BrunoError('Unable to parse the postman environment json file'));
|
||||
console.error(`Error processing file: ${parsedFile.fileName}`, err);
|
||||
throw new BrunoError(`Failed to process ${parsedFile.fileName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const importEnvironment = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ multiple: true, accept: 'application/json' })
|
||||
.then((files) => {
|
||||
return Promise.all(
|
||||
Object.values(files ?? {}).map((file) =>
|
||||
readFile([file])
|
||||
.then((environment) => postmanToBrunoEnvironment(environment))
|
||||
.catch((err) => {
|
||||
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
.then((environments) => resolve(environments))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import Environment failed'));
|
||||
});
|
||||
});
|
||||
return environments;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err instanceof BrunoError ? err : new BrunoError('Import Environment failed');
|
||||
}
|
||||
};
|
||||
|
||||
export default importEnvironment;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const fsPromises = require('fs/promises');
|
||||
const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
@@ -14,7 +13,6 @@ const {
|
||||
stringifyCollection,
|
||||
parseFolder,
|
||||
stringifyFolder,
|
||||
parseEnvironment,
|
||||
stringifyEnvironment
|
||||
} = require('@usebruno/filestore');
|
||||
const brunoConverters = require('@usebruno/converters');
|
||||
@@ -27,8 +25,6 @@ const {
|
||||
writeFile,
|
||||
hasBruExtension,
|
||||
isDirectory,
|
||||
browseDirectory,
|
||||
browseFiles,
|
||||
createDirectory,
|
||||
searchForBruFiles,
|
||||
sanitizeName,
|
||||
@@ -42,7 +38,8 @@ const {
|
||||
safeWriteFileSync,
|
||||
copyPath,
|
||||
removePath,
|
||||
getPaths
|
||||
getPaths,
|
||||
generateUniqueName
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@@ -298,13 +295,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
await createDirectory(envDirPath);
|
||||
}
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${name}.bru`);
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} already exists`);
|
||||
}
|
||||
// Get existing environment files to generate unique name
|
||||
const existingFiles = fs.existsSync(envDirPath) ? fs.readdirSync(envDirPath) : [];
|
||||
const existingEnvNames = existingFiles
|
||||
.filter((file) => file.endsWith('.bru'))
|
||||
.map((file) => path.basename(file, '.bru'));
|
||||
|
||||
// Generate unique name based on existing environment files
|
||||
const sanitizedName = sanitizeName(name);
|
||||
const uniqueName = generateUniqueName(sanitizedName, (name) => existingEnvNames.includes(name));
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${uniqueName}.bru`);
|
||||
|
||||
const environment = {
|
||||
name: name,
|
||||
name: uniqueName,
|
||||
variables: variables || []
|
||||
};
|
||||
|
||||
@@ -383,6 +387,77 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
// Generic environment export handler
|
||||
ipcMain.handle('renderer:export-environment', async (event, { environments, environmentType, filePath, exportFormat = 'folder' }) => {
|
||||
try {
|
||||
const { app } = require('electron');
|
||||
const appVersion = app?.getVersion() || '2.0.0';
|
||||
|
||||
// For single environments and folder exports, include info in each environment
|
||||
const environmentWithInfo = (environment) => ({
|
||||
name: environment.name,
|
||||
variables: environment.variables,
|
||||
info: {
|
||||
type: 'bruno-environment',
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedUsing: `Bruno/v${appVersion}`
|
||||
}
|
||||
});
|
||||
|
||||
if (exportFormat === 'folder') {
|
||||
// separate environment json files in folder
|
||||
const baseFolderName = `bruno-${environmentType}-environments`;
|
||||
const uniqueFolderName = generateUniqueName(baseFolderName, (name) => fs.existsSync(path.join(filePath, name)));
|
||||
const exportPath = path.join(filePath, uniqueFolderName);
|
||||
|
||||
fs.mkdirSync(exportPath, { recursive: true });
|
||||
|
||||
for (const environment of environments) {
|
||||
const baseFileName = `${environment.name.replace(/[^a-zA-Z0-9-_]/g, '_')}`;
|
||||
const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(exportPath, `${name}.json`)));
|
||||
const fullPath = path.join(exportPath, `${uniqueFileName}.json`);
|
||||
|
||||
const cleanEnv = environmentWithInfo(environment);
|
||||
const jsonContent = JSON.stringify(cleanEnv, null, 2);
|
||||
await fs.promises.writeFile(fullPath, jsonContent, 'utf8');
|
||||
}
|
||||
} else if (exportFormat === 'single-file') {
|
||||
// all environments in a single file with top-level info and environments array
|
||||
const baseFileName = `bruno-${environmentType}-environments`;
|
||||
const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(filePath, `${name}.json`)));
|
||||
const fullPath = path.join(filePath, `${uniqueFileName}.json`);
|
||||
|
||||
const exportData = {
|
||||
info: {
|
||||
type: 'bruno-environment',
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedUsing: `Bruno/v${appVersion}`
|
||||
},
|
||||
environments
|
||||
};
|
||||
|
||||
const jsonContent = JSON.stringify(exportData, null, 2);
|
||||
await fs.promises.writeFile(fullPath, jsonContent, 'utf8');
|
||||
} else if (exportFormat === 'single-object') {
|
||||
// single environment json file
|
||||
if (environments.length !== 1) {
|
||||
throw new Error('Single object export requires exactly one environment');
|
||||
}
|
||||
|
||||
const environment = environments[0];
|
||||
const baseFileName = `${environment.name.replace(/[^a-zA-Z0-9-_]/g, '_')}`;
|
||||
const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(filePath, `${name}.json`)));
|
||||
const fullPath = path.join(filePath, `${uniqueFileName}.json`);
|
||||
const jsonContent = JSON.stringify(environmentWithInfo(environment), null, 2);
|
||||
await fs.promises.writeFile(fullPath, jsonContent, 'utf8');
|
||||
} else {
|
||||
throw new Error(`Unsupported export format: ${exportFormat}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName }) => {
|
||||
try {
|
||||
|
||||
@@ -50,4 +50,4 @@ const registerFilesystemIpc = (mainWindow) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = registerFilesystemIpc;
|
||||
module.exports = registerFilesystemIpc;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('dotenv').config();
|
||||
const { ipcMain } = require('electron');
|
||||
const { globalEnvironmentsStore } = require('../store/global-environments');
|
||||
const { generateUniqueName, sanitizeName } = require('../utils/filesystem');
|
||||
|
||||
const registerGlobalEnvironmentsIpc = (mainWindow) => {
|
||||
|
||||
@@ -8,7 +9,18 @@ const registerGlobalEnvironmentsIpc = (mainWindow) => {
|
||||
|
||||
ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables }) => {
|
||||
try {
|
||||
globalEnvironmentsStore.addGlobalEnvironment({ uid, name, variables });
|
||||
// Get existing global environment names to generate unique name
|
||||
const existingGlobalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
|
||||
const existingNames = existingGlobalEnvironments?.map((env) => env.name) || [];
|
||||
|
||||
// Generate unique name based on existing global environment names
|
||||
const sanitizedName = sanitizeName(name);
|
||||
const uniqueName = generateUniqueName(sanitizedName, (name) => existingNames.includes(name));
|
||||
|
||||
globalEnvironmentsStore.addGlobalEnvironment({ uid, name: uniqueName, variables });
|
||||
|
||||
// Return the unique name that was actually used
|
||||
return { name: uniqueName };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,29 @@ const sanitizeName = (name) => {
|
||||
|
||||
const isWindowsOS = () => {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique name by adding a "copy" suffix if needed
|
||||
*
|
||||
* @param {string} baseName - The base name
|
||||
* @param {Function} checkExists - Function that takes a name and returns true if it exists
|
||||
* @returns {string} - A unique name
|
||||
*/
|
||||
const generateUniqueName = (baseName, checkExists) => {
|
||||
if (!checkExists(baseName)) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
let counter = 1;
|
||||
let uniqueName = `${baseName} copy`;
|
||||
|
||||
while (checkExists(uniqueName)) {
|
||||
counter++;
|
||||
uniqueName = `${baseName} copy ${counter}`;
|
||||
}
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // keeping this for informational purpose
|
||||
@@ -389,5 +411,6 @@ module.exports = {
|
||||
copyPath,
|
||||
removePath,
|
||||
getPaths,
|
||||
isLargeFile
|
||||
isLargeFile,
|
||||
generateUniqueName
|
||||
};
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import { test, expect } from '../../../../playwright';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Helper function to load expected fixtures
|
||||
function loadExpectedFixture(fixturePath: string) {
|
||||
const fullPath = path.join(__dirname, '..', '../fixtures', 'environment-exports', fixturePath);
|
||||
console.log(fullPath);
|
||||
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
}
|
||||
|
||||
// Helper function to normalize dynamic fields for comparison
|
||||
function normalizeExportedContent(content: any) {
|
||||
if (content.info) {
|
||||
// Replace dynamic fields with fixed values for comparison
|
||||
content.info.exportedAt = '2024-01-01T00:00:00.000Z';
|
||||
content.info.exportedUsing = 'Bruno/v1.0.0';
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
test.describe.serial('Collection Environment Export Tests', () => {
|
||||
test.describe.serial('folder exports', () => {
|
||||
test('should export single collection environment', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-single');
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the environment settings modal opens
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and configure export settings', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Verify export modal opens
|
||||
const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });
|
||||
await expect(exportModal).toBeVisible();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'local.json');
|
||||
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-collection-environments/local.json');
|
||||
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should export multiple collection environments', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-multiple');
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for multiple environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Verify all environments are selected by default
|
||||
await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();
|
||||
await expect(page.getByRole('checkbox', { name: 'Prod' })).toBeChecked();
|
||||
|
||||
// Select folder export format (default might be single JSON file)
|
||||
await page.getByText('Separate files in folder').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export all environments
|
||||
await page.getByRole('button', { name: /Export \d+ Environments?/ }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported files and content', async () => {
|
||||
// Verify exported files exist
|
||||
const exportPath = path.join(exportDir, 'bruno-collection-environments');
|
||||
expect(fs.existsSync(exportPath)).toBe(true);
|
||||
|
||||
const expectedFiles = [
|
||||
'local.json',
|
||||
'prod.json'
|
||||
];
|
||||
|
||||
for (const fileName of expectedFiles) {
|
||||
const filePath = path.join(exportPath, fileName);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture(`bruno-collection-environments/${fileName}`);
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should generate unique names when the export directory already contains previously exported contents', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-conflict');
|
||||
|
||||
await test.step('Setup existing export directory to simulate conflict', async () => {
|
||||
// Create existing export directory and file to simulate conflict
|
||||
const existingExportPath = path.join(exportDir, 'bruno-collection-environments');
|
||||
fs.mkdirSync(existingExportPath, { recursive: true });
|
||||
fs.writeFileSync(path.join(existingExportPath, 'local.json'), '{}');
|
||||
});
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings with folder format', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Select folder export format (default might be single JSON file)
|
||||
await page.getByText('Separate files in folder').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 2 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
// Verify original folder still exists
|
||||
const existingExportPath = path.join(exportDir, 'bruno-collection-environments');
|
||||
expect(fs.existsSync(existingExportPath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(existingExportPath, 'local.json'))).toBe(true);
|
||||
|
||||
// Verify new folder with unique name was created
|
||||
const uniqueExportPath = path.join(exportDir, 'bruno-collection-environments copy');
|
||||
expect(fs.existsSync(uniqueExportPath)).toBe(true);
|
||||
|
||||
// Verify the new file exists in the unique folder
|
||||
const newExportedFile = path.join(uniqueExportPath, 'local.json');
|
||||
expect(fs.existsSync(newExportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(newExportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-collection-environments/local.json');
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe.serial('json file exports', () => {
|
||||
test('should export single collection environment as object', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-single-object');
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open collection environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and verify success', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'local.json');
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('local.json');
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should export multiple collection environments as single JSON file', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-single-file');
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open collection environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Select single JSON file format
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and verify success', async () => {
|
||||
// Export the environments
|
||||
await page.getByRole('button', { name: 'Export 2 Environments' }).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'bruno-collection-environments.json');
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-collection-environments.json');
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should generate unique names when the export directory already contains previously exported contents', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-single-object');
|
||||
|
||||
await test.step('Setup existing export file to simulate conflict', async () => {
|
||||
// Create existing export directory and file to simulate conflict
|
||||
const existingExportJsonPath = path.join(exportDir, 'local.json');
|
||||
fs.writeFileSync(existingExportJsonPath, '{}');
|
||||
});
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open collection environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
// Verify original file still exists
|
||||
const existingExportJsonPath = path.join(exportDir, 'local.json');
|
||||
expect(fs.existsSync(existingExportJsonPath)).toBe(true);
|
||||
|
||||
// Verify new file with unique name was created
|
||||
const uniqueExportPath = path.join(exportDir, 'local copy.json');
|
||||
expect(fs.existsSync(uniqueExportPath)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(uniqueExportPath, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('local.json');
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// common tests
|
||||
test('should not be able to export, when no environments are selected', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('collection-env-export-no-selection');
|
||||
|
||||
await test.step('Open collection and navigate to environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and deselect all environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments
|
||||
await page.getByText('Deselect All').click();
|
||||
});
|
||||
|
||||
await test.step('Verify export button is disabled when no environments selected', async () => {
|
||||
// Verify export button is disabled
|
||||
await expect(page.getByRole('button', { name: 'Export Environments' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Environment Export Test Collection",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
vars {
|
||||
host: http://localhost:3000
|
||||
}
|
||||
|
||||
vars:secret [
|
||||
secretToken
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
vars {
|
||||
host: https://echo.usebruno.com
|
||||
}
|
||||
|
||||
vars:secret [
|
||||
secretToken
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
meta {
|
||||
name: Test Request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"environments": [
|
||||
{
|
||||
"uid": "local-env-export-test",
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"uid": "local-env-export-test-host",
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"uid": "local-env-export-test-secret-token",
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "prod-env-export-test",
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"uid": "prod-env-export-test-host",
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"uid": "prod-env-export-test-secret-token",
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"activeGlobalEnvironmentUid": "local-env-export-test"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Environment Export Test Collection",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
meta {
|
||||
name: Test Request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import { test, expect } from '../../../../playwright';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Helper function to load expected fixtures
|
||||
function loadExpectedFixture(fixturePath: string) {
|
||||
const fullPath = path.join(__dirname, '..', '../fixtures', 'environment-exports', fixturePath);
|
||||
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
}
|
||||
|
||||
// Helper function to normalize dynamic fields for comparison
|
||||
function normalizeExportedContent(content: any) {
|
||||
if (content.info) {
|
||||
// Replace dynamic fields with fixed values for comparison
|
||||
content.info.exportedAt = '2024-01-01T00:00:00.000Z';
|
||||
content.info.exportedUsing = 'Bruno/v1.0.0';
|
||||
}
|
||||
// Handle individual environment files that have info at the root level
|
||||
if (content.name && content.variables && content.info) {
|
||||
content.info.exportedAt = '2024-01-01T00:00:00.000Z';
|
||||
content.info.exportedUsing = 'Bruno/v1.0.0';
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
test.describe.serial('Global Environment Export Tests', () => {
|
||||
test.describe.serial('folder exports', () => {
|
||||
test('should export single global environment', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-single');
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the global environment settings modal opens
|
||||
const globalEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(globalEnvModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and configure export settings', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Verify export modal opens
|
||||
const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });
|
||||
await expect(exportModal).toBeVisible();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'local.json');
|
||||
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-global-environments/local.json');
|
||||
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should export multiple global environments', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-multiple');
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for multiple environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Verify all environments are selected by default
|
||||
await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();
|
||||
await expect(page.getByRole('checkbox', { name: 'Prod' })).toBeChecked();
|
||||
|
||||
// Select folder export format (default might be single JSON file)
|
||||
await page.getByText('Separate files in folder').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export all environments
|
||||
await page.getByRole('button', { name: /Export \d+ Environments?/ }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported files and content', async () => {
|
||||
// Verify exported files exist
|
||||
const exportPath = path.join(exportDir, 'bruno-global-environments');
|
||||
expect(fs.existsSync(exportPath)).toBe(true);
|
||||
|
||||
const expectedFiles = [
|
||||
'local.json',
|
||||
'prod.json'
|
||||
];
|
||||
|
||||
for (const fileName of expectedFiles) {
|
||||
const filePath = path.join(exportPath, fileName);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture(`bruno-global-environments/${fileName}`);
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should generate unique names when the export directory already contains previously exported contents', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-conflict');
|
||||
|
||||
await test.step('Setup existing export directory to simulate conflict', async () => {
|
||||
// Create existing export directory and file to simulate conflict
|
||||
const existingExportPath = path.join(exportDir, 'bruno-global-environments');
|
||||
fs.mkdirSync(existingExportPath, { recursive: true });
|
||||
fs.writeFileSync(path.join(existingExportPath, 'local.json'), '{}');
|
||||
});
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings with folder format', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
|
||||
// Select folder export format
|
||||
await page.getByText('Separate files in folder').click();
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 2 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').first().click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
// Verify original folder still exists
|
||||
const existingExportPath = path.join(exportDir, 'bruno-global-environments');
|
||||
expect(fs.existsSync(existingExportPath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(existingExportPath, 'local.json'))).toBe(true);
|
||||
|
||||
// Verify new folder with unique name was created
|
||||
const uniqueExportPath = path.join(exportDir, 'bruno-global-environments copy');
|
||||
expect(fs.existsSync(uniqueExportPath)).toBe(true);
|
||||
|
||||
// Verify the new file exists in the unique folder
|
||||
const newExportedFile = path.join(uniqueExportPath, 'local.json');
|
||||
expect(fs.existsSync(newExportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(newExportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-global-environments/local.json');
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe.serial('json file exports', () => {
|
||||
test('should export single global environment as object', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-single-object');
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
// Single JSON file format is automatically selected for single environment
|
||||
// The backend will automatically use 'single-object' format for single environment
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and verify success', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false })).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'local.json');
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('local.json');
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should export multiple global environments as single JSON file', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-single-file');
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Select single JSON file format
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and verify success', async () => {
|
||||
// Export the environments
|
||||
await page.getByRole('button', { name: 'Export 2 Environments' }).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false })).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
// Verify exported file exists
|
||||
const exportedFile = path.join(exportDir, 'bruno-global-environments.json');
|
||||
expect(fs.existsSync(exportedFile)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('bruno-global-environments.json');
|
||||
expect(normalizeExportedContent(content)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
test('should generate unique names when the export directory already contains previously exported contents', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-single-object-conflict');
|
||||
|
||||
await test.step('Setup existing export file to simulate conflict', async () => {
|
||||
// Create existing export directory and file to simulate conflict
|
||||
const existingExportJsonPath = path.join(exportDir, 'local.json');
|
||||
fs.writeFileSync(existingExportJsonPath, '{}');
|
||||
});
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
// Select only "local" environment
|
||||
const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type="checkbox"]');
|
||||
await localEnvCheckbox.check();
|
||||
|
||||
// Single JSON file format is automatically selected for single environment
|
||||
// The backend will automatically use 'single-object' format for single environment
|
||||
await page.getByText('Single JSON file').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
});
|
||||
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').first().click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
// Verify original file still exists
|
||||
const existingExportJsonPath = path.join(exportDir, 'local.json');
|
||||
expect(fs.existsSync(existingExportJsonPath)).toBe(true);
|
||||
|
||||
// Verify new file with unique name was created
|
||||
const uniqueExportPath = path.join(exportDir, 'local copy.json');
|
||||
expect(fs.existsSync(uniqueExportPath)).toBe(true);
|
||||
|
||||
// Verify file content matches expected fixture
|
||||
const exportedContent = JSON.parse(fs.readFileSync(uniqueExportPath, 'utf8'));
|
||||
const expectedContent = loadExpectedFixture('local.json');
|
||||
expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// common tests
|
||||
test('should not be able to export, when no environments are selected', async ({
|
||||
pageWithUserData: page,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const exportDir = await createTmpDir('global-env-export-no-selection');
|
||||
|
||||
await test.step('Open collection and navigate to global environment settings', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();
|
||||
|
||||
// Open global environment settings
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and deselect all environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
|
||||
// Deselect all environments
|
||||
await page.getByText('Deselect All').click();
|
||||
});
|
||||
|
||||
await test.step('Verify export button is disabled when no environments selected', async () => {
|
||||
// Verify export button is disabled
|
||||
await expect(page.getByRole('button', { name: 'Export Environments' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"environments": [
|
||||
{
|
||||
"uid": "local-env-export-test",
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"uid": "local-env-export-test-host",
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"uid": "local-env-export-test-secret-token",
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "prod-env-export-test",
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"uid": "prod-env-export-test-host",
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"uid": "prod-env-export-test-secret-token",
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"activeGlobalEnvironmentUid": "local-env-export-test"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
},
|
||||
"environments": [
|
||||
{
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
},
|
||||
"environments": [
|
||||
{
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "prod",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "https://echo.usebruno.com",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
}
|
||||
}
|
||||
24
tests/environments/fixtures/environment-exports/local.json
Normal file
24
tests/environments/fixtures/environment-exports/local.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "host",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"name": "secretToken",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"enabled": true,
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"type": "bruno-environment",
|
||||
"exportedAt": "2024-01-01T00:00:00.000Z",
|
||||
"exportedUsing": "Bruno/v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { test, expect } from '../../../../../playwright';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
test.describe.serial('Collection Environment Import Tests', () => {
|
||||
test('should import single collection environment', async ({ pageWithUserData: page }) => {
|
||||
const singleEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/local.json');
|
||||
const collectionPath = path.join(__dirname, 'fixtures/collection');
|
||||
const environmentsPath = path.join(collectionPath, 'environments');
|
||||
|
||||
await test.step('Clean up existing environments and open collection', async () => {
|
||||
// Clean up any existing environments folder before test
|
||||
if (fs.existsSync(environmentsPath)) {
|
||||
fs.rmSync(environmentsPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();
|
||||
});
|
||||
|
||||
await test.step('Navigate to collection environment import', async () => {
|
||||
// Open environment import
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await expect(page.getByTestId('env-tab-collection')).toHaveClass(/active/);
|
||||
await page.getByText('Import', { exact: true }).click();
|
||||
|
||||
// Verify import modal opens
|
||||
const importModal = page.locator('[data-testid="import-environment-modal"]');
|
||||
await expect(importModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Import environment file', async () => {
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByTestId('import-environment').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(singleEnvFile);
|
||||
});
|
||||
|
||||
await test.step('Verify imported environment and variables', async () => {
|
||||
// The environment settings modal should now be visible with the imported environment
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
// Clean up any existing environments folder before test
|
||||
if (fs.existsSync(environmentsPath)) {
|
||||
fs.rmSync(environmentsPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should import multiple collection environments', async ({ pageWithUserData: page }) => {
|
||||
const multiEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/bruno-collection-environments.json');
|
||||
const collectionPath = path.join(__dirname, 'fixtures/collection');
|
||||
const environmentsPath = path.join(collectionPath, 'environments');
|
||||
|
||||
await test.step('Clean up existing environments and open collection', async () => {
|
||||
// Clean up any existing environments folder before test
|
||||
if (fs.existsSync(environmentsPath)) {
|
||||
fs.rmSync(environmentsPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();
|
||||
});
|
||||
|
||||
await test.step('Navigate to collection environment import', async () => {
|
||||
// Open environment import
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await expect(page.getByTestId('env-tab-collection')).toHaveClass(/active/);
|
||||
await page.getByText('Import', { exact: true }).click();
|
||||
|
||||
// Verify import modal opens
|
||||
const importModal = page.locator('[data-testid="import-environment-modal"]');
|
||||
await expect(importModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Import multiple environments file', async () => {
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByTestId('import-environment').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(multiEnvFile);
|
||||
|
||||
// The environment settings modal should now be visible with the imported environments
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify both environments are available in selector', async () => {
|
||||
// Check that both environments are available in the selector
|
||||
await page.getByText('×').click(); // Close environment settings modal
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
|
||||
// Verify both environments are in the dropdown
|
||||
await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible();
|
||||
await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test switching to prod environment and verify variables', async () => {
|
||||
// Test switching to prod environment
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'prod' }).click();
|
||||
await expect(page.locator('.current-environment')).toContainText('prod');
|
||||
|
||||
// Verify prod environment variables by opening settings again
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
|
||||
// Verify prod environment variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
// Clean up any existing environments folder before test
|
||||
if (fs.existsSync(environmentsPath)) {
|
||||
fs.rmSync(environmentsPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Environment Import Test Collection",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
meta {
|
||||
name: Test Request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Environment Import Test Collection",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
meta {
|
||||
name: Test Request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { test, expect } from '../../../../../playwright';
|
||||
import path from 'path';
|
||||
|
||||
test.describe.serial('Global Environment Import Tests', () => {
|
||||
test('should import single global environment', async ({ pageWithUserData: page }) => {
|
||||
const singleEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/local.json');
|
||||
|
||||
await test.step('Open collection and clean up existing global environments', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();
|
||||
|
||||
// Clean up any existing global environments before test
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
|
||||
// Check if there are existing environments to delete
|
||||
const existingEnvs = page.locator('.dropdown-item').filter({ hasText: /^(local|prod)$/ });
|
||||
const count = await existingEnvs.count();
|
||||
|
||||
if (count > 0) {
|
||||
// Open global environment settings to delete existing environments
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Delete all existing environments
|
||||
for (let i = 0; i < count; i++) {
|
||||
await page.getByTestId('delete-environment-button').click();
|
||||
// Confirm deletion if there's a confirmation dialog
|
||||
const confirmButton = page.getByRole('button', { name: 'Delete' });
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.getByText('×').click();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any existing global environments before test
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('Navigate to global environment import', async () => {
|
||||
await expect(page.getByTestId('env-tab-global')).toHaveClass(/active/);
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify import modal opens
|
||||
const importModal = page.locator('[data-testid="import-global-environment-modal"]');
|
||||
await expect(importModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Import global environment file', async () => {
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('[data-testid="import-global-environment"]').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(singleEnvFile);
|
||||
});
|
||||
|
||||
await test.step('Verify imported global environment and variables', async () => {
|
||||
// The global environment settings modal should now be visible with the imported environment
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
});
|
||||
});
|
||||
|
||||
test('should import multiple global environments', async ({ pageWithUserData: page }) => {
|
||||
const multiEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/bruno-global-environments.json');
|
||||
|
||||
await test.step('Open collection and clean up existing global environments', async () => {
|
||||
// Open the collection from sidebar
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();
|
||||
|
||||
// Clean up any existing global environments before test
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
|
||||
// Check if there are existing environments to delete
|
||||
const existingEnvs = page.locator('.dropdown-item').filter({ hasText: /^(local|prod)$/ });
|
||||
const count = await existingEnvs.count();
|
||||
|
||||
if (count > 0) {
|
||||
// Open global environment settings to delete existing environments
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Delete all existing environments
|
||||
for (let i = 0; i < count; i++) {
|
||||
await page.getByTestId('delete-environment-button').click();
|
||||
// Confirm deletion if there's a confirmation dialog
|
||||
const confirmButton = page.getByRole('button', { name: 'Delete' });
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.getByText('×').click();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any existing global environments before test
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('Navigate to global environment import', async () => {
|
||||
await page.getByText('Import', { exact: true }).click();
|
||||
|
||||
// Verify import modal opens
|
||||
const importModal = page.locator('[data-testid="import-global-environment-modal"]');
|
||||
await expect(importModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Import multiple global environments file', async () => {
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('[data-testid="import-global-environment"]').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(multiEnvFile);
|
||||
|
||||
// The global environment settings modal should now be visible with the imported environments
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify both global environments are available in selector', async () => {
|
||||
// Check that both environments are available in the selector
|
||||
await page.getByText('×').click(); // Close environment settings modal
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
|
||||
// Verify both environments are in the dropdown
|
||||
await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible();
|
||||
await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test switching to prod environment and verify variables', async () => {
|
||||
// Test switching to prod environment
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'prod' }).click();
|
||||
await expect(page.locator('.current-environment')).toContainText('prod');
|
||||
|
||||
// Verify prod environment variables by opening settings again
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -40,7 +40,7 @@ test.describe('Collection Environment Import Tests', () => {
|
||||
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('button[data-testid="import-postman-environment"]').click();
|
||||
await page.getByTestId('import-environment').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(envFile);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { test, expect } from '../../../playwright';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('Global Environment Import Tests', () => {
|
||||
test('should import global environment from file', async ({ page, createTmpDir }) => {
|
||||
test('should import global environment from file', async ({ newPage: page, createTmpDir }) => {
|
||||
const openApiFile = path.join(__dirname, 'fixtures', 'collection.json');
|
||||
const globalEnvFile = path.join(__dirname, 'fixtures', 'global-env.json');
|
||||
|
||||
@@ -32,16 +32,15 @@ test.describe('Global Environment Import Tests', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Import global environment
|
||||
await page.locator('[data-testid="environment-selector-trigger"]').click();
|
||||
await page.locator('[data-testid="env-tab-global"]').click();
|
||||
await expect(page.locator('[data-testid="env-tab-global"]')).toHaveClass(/active/);
|
||||
await page.locator('button[id="import-env"]').click();
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Import', { exact: true }).click();
|
||||
const importGlobalEnvModal = page.locator('[data-testid="import-global-environment-modal"]');
|
||||
await expect(importGlobalEnvModal).toBeVisible();
|
||||
|
||||
// Import environment file
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('button[data-testid="import-postman-global-environment"]').click();
|
||||
await page.locator('[data-testid="import-global-environment"]').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(globalEnvFile);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user