diff --git a/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/StyledWrapper.js b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/StyledWrapper.js new file mode 100644 index 000000000..d33e7726e --- /dev/null +++ b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js new file mode 100644 index 000000000..fab282b64 --- /dev/null +++ b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js @@ -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 ( + + + +
+ {/* Environments Section */} +
+ {environments && environments.length > 0 ? ( +
+
+

+ {environmentType === 'global' ? 'Global Environments' : 'Collection Environments'} +

+ +
+
+ {environments.map((env) => ( + + ))} +
+
+ ) : ( +
+
+

+ {environmentType === 'global' ? 'Global Environments' : 'Collection Environments'} +

+
+
+ + No {environmentType === 'global' ? 'global' : 'collection'} environments + +
+
+ )} +
+ + {/* Export Format Section */} + {selectedCount > 0 && ( +
+ +
+ {exportFormatOptions.map((option) => ( + + ))} +
+
+ )} + + {/* Location Input Section */} +
+ +
+ setFilePath(e.target.value)} + disabled={isExporting || selectedCount <= 0} + placeholder="Select a target location" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + /> +
+
+ + {/* Export Actions */} +
+ + +
+
+
+
+
+ ); +}; + +export default ExportEnvironmentModal; diff --git a/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js b/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js new file mode 100644 index 000000000..5617dd435 --- /dev/null +++ b/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js @@ -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 ( + + +
+
+ + + {isDragOver ? 'Drop your environment files here' : 'Import your environments'} + + + Drag & drop JSON files/folders or click to browse. Supports both Bruno and Postman formats. + +
+
+
+
+ ); +}; + +export default ImportEnvironmentModal; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index 1e74bb7d4..31763036b 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -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 && ( - setShowImportGlobalModal(false)} onEnvironmentCreated={() => { setShowGlobalSettings(true); @@ -261,7 +261,8 @@ const EnvironmentSelector = ({ collection }) => { )} {showImportCollectionModal && ( - setShowImportCollectionModal(false)} onEnvironmentCreated={() => { diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js index 17c0bbcf0..72418f693 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js @@ -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 }) {environment.name} -
- setOpenEditModal(true)} /> - setOpenCopyModal(true)} /> - setOpenDeleteModal(true)} /> +
+ + setOpenEditModal(true)} /> + + + setOpenCopyModal(true)} /> + + + setOpenDeleteModal(true)} + data-testid="delete-environment-button" + /> +
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js index 278a7f25d..908854261 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js @@ -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 ( {openCreateModal && setOpenCreateModal(false)} />} - {openImportModal && setOpenImportModal(false)} />} + {openImportModal && setOpenImportModal(false)} />} {openManageSecretsModal && setOpenManageSecretsModal(false)} />}
@@ -129,6 +129,10 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti Import
+
setShowExportModal(true)}> + + Export +
handleSecretsClick()}> Managing Secrets diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js deleted file mode 100644 index e962780c5..000000000 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js +++ /dev/null @@ -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 ( - - - - - - ); -}; - -export default ImportEnvironment; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js index 1fa61913e..419db7c41 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js @@ -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 ( @@ -54,7 +56,7 @@ const EnvironmentSettings = ({ collection, onClose }) => { {tab === 'create' ? ( setTab('default')} /> ) : tab === 'import' ? ( - setTab('default')} /> + setTab('default')} /> ) : ( )} @@ -64,16 +66,26 @@ const EnvironmentSettings = ({ collection, onClose }) => { } return ( - - - + + + + + {showExportModal && ( + setShowExportModal(false)} + environments={collection.environments} + environmentType="collection" + /> + )} + ); }; diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js index 795f8e7cc..3cabf04c2 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js @@ -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 }) => { {environment.name}
-
- setOpenEditModal(true)} /> - setOpenCopyModal(true)} /> - setOpenDeleteModal(true)} /> +
+ + setOpenEditModal(true)} /> + + + setOpenCopyModal(true)} /> + + + setOpenDeleteModal(true)} data-testid="delete-environment-button" /> +
- +
); diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js index c99459efe..3a7e024e7 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js @@ -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 ( {openCreateModal && setOpenCreateModal(false)} />} - {openImportModal && setOpenImportModal(false)} />} + {openImportModal && setOpenImportModal(false)} />} {openManageSecretsModal && setOpenManageSecretsModal(false)} />}
@@ -132,6 +138,10 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme Import
+
handleExportClick()}> + + Export +
handleSecretsClick()}> Managing Secrets @@ -144,6 +154,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme setIsModified={setIsModified} originalEnvironmentVariables={originalEnvironmentVariables} collection={collection} + allEnvironments={environments} />
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js deleted file mode 100644 index 6e56472d8..000000000 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js +++ /dev/null @@ -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 ( - - - - - - ); -}; - -export default ImportEnvironment; diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js index 07aa0e667..c49cacbf5 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js @@ -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 ( @@ -51,7 +53,7 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir {tab === 'create' ? ( setTab('default')} /> ) : tab === 'import' ? ( - setTab('default')} /> + setTab('default')} /> ) : ( )} @@ -61,17 +63,27 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir } return ( - - - + + + + + {showExportModal && ( + setShowExportModal(false)} + environments={globalEnvironments} + environmentType="global" + /> + )} + ); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index cee1fb201..c886dece9 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -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); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js index 97e1d8784..6fe856ce5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -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); }); diff --git a/packages/bruno-app/src/utils/environments.js b/packages/bruno-app/src/utils/environments.js index 50fcfcdf7..f7e8948cb 100644 --- a/packages/bruno-app/src/utils/environments.js +++ b/packages/bruno-app/src/utils/environments.js @@ -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 + }; +}; diff --git a/packages/bruno-app/src/utils/exporters/bruno-environment.js b/packages/bruno-app/src/utils/exporters/bruno-environment.js new file mode 100644 index 000000000..e22bc8dcd --- /dev/null +++ b/packages/bruno-app/src/utils/exporters/bruno-environment.js @@ -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.`); + } +}; diff --git a/packages/bruno-app/src/utils/importers/bruno-environment.js b/packages/bruno-app/src/utils/importers/bruno-environment.js new file mode 100644 index 000000000..13a37380e --- /dev/null +++ b/packages/bruno-app/src/utils/importers/bruno-environment.js @@ -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; diff --git a/packages/bruno-app/src/utils/importers/file-reader.js b/packages/bruno-app/src/utils/importers/file-reader.js new file mode 100644 index 000000000..fd12c81a7 --- /dev/null +++ b/packages/bruno-app/src/utils/importers/file-reader.js @@ -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; +}; diff --git a/packages/bruno-app/src/utils/importers/postman-environment.js b/packages/bruno-app/src/utils/importers/postman-environment.js index e4185deb5..74197b3ef 100644 --- a/packages/bruno-app/src/utils/importers/postman-environment.js +++ b/packages/bruno-app/src/utils/importers/postman-environment.js @@ -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; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 6cdb4ea90..1f81cb1b1 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -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 { diff --git a/packages/bruno-electron/src/ipc/filesystem.js b/packages/bruno-electron/src/ipc/filesystem.js index bdd8dfc1a..ecdae948f 100644 --- a/packages/bruno-electron/src/ipc/filesystem.js +++ b/packages/bruno-electron/src/ipc/filesystem.js @@ -50,4 +50,4 @@ const registerFilesystemIpc = (mainWindow) => { }); }; -module.exports = registerFilesystemIpc; \ No newline at end of file +module.exports = registerFilesystemIpc; diff --git a/packages/bruno-electron/src/ipc/global-environments.js b/packages/bruno-electron/src/ipc/global-environments.js index dc7258ee1..40e6ccb7e 100644 --- a/packages/bruno-electron/src/ipc/global-environments.js +++ b/packages/bruno-electron/src/ipc/global-environments.js @@ -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); } diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index 00892e903..65ea4a106 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -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 }; diff --git a/tests/environments/export-environment/collection-env-export/collection-env-export.spec.ts b/tests/environments/export-environment/collection-env-export/collection-env-export.spec.ts new file mode 100644 index 000000000..712cda34f --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/collection-env-export.spec.ts @@ -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(); + }); + }); +}); diff --git a/tests/environments/export-environment/collection-env-export/fixtures/collection/bruno.json b/tests/environments/export-environment/collection-env-export/fixtures/collection/bruno.json new file mode 100644 index 000000000..6690324d5 --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/fixtures/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Environment Export Test Collection", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/local.bru b/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/local.bru new file mode 100644 index 000000000..8e5eb4a6a --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/local.bru @@ -0,0 +1,7 @@ +vars { + host: http://localhost:3000 +} + +vars:secret [ + secretToken +] \ No newline at end of file diff --git a/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/prod.bru b/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/prod.bru new file mode 100644 index 000000000..54d3bbe1b --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/fixtures/collection/environments/prod.bru @@ -0,0 +1,7 @@ +vars { + host: https://echo.usebruno.com +} + +vars:secret [ + secretToken +] \ No newline at end of file diff --git a/tests/environments/export-environment/collection-env-export/fixtures/collection/test-request.bru b/tests/environments/export-environment/collection-env-export/fixtures/collection/test-request.bru new file mode 100644 index 000000000..69f9639d1 --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/fixtures/collection/test-request.bru @@ -0,0 +1,9 @@ +meta { + name: Test Request + type: http + seq: 1 +} + +get { + url: {{host}} +} diff --git a/tests/environments/export-environment/collection-env-export/init-user-data/collection-security.json b/tests/environments/export-environment/collection-env-export/init-user-data/collection-security.json new file mode 100644 index 000000000..08536a112 --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/export-environment/collection-env-export/init-user-data/global-environments.json b/tests/environments/export-environment/collection-env-export/init-user-data/global-environments.json new file mode 100644 index 000000000..d45fa4c26 --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/init-user-data/global-environments.json @@ -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" +} diff --git a/tests/environments/export-environment/collection-env-export/init-user-data/preferences.json b/tests/environments/export-environment/collection-env-export/init-user-data/preferences.json new file mode 100644 index 000000000..a43114034 --- /dev/null +++ b/tests/environments/export-environment/collection-env-export/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection" + ] +} diff --git a/tests/environments/export-environment/global-env-export/fixtures/collection/bruno.json b/tests/environments/export-environment/global-env-export/fixtures/collection/bruno.json new file mode 100644 index 000000000..6690324d5 --- /dev/null +++ b/tests/environments/export-environment/global-env-export/fixtures/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Environment Export Test Collection", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/environments/export-environment/global-env-export/fixtures/collection/test-request.bru b/tests/environments/export-environment/global-env-export/fixtures/collection/test-request.bru new file mode 100644 index 000000000..69f9639d1 --- /dev/null +++ b/tests/environments/export-environment/global-env-export/fixtures/collection/test-request.bru @@ -0,0 +1,9 @@ +meta { + name: Test Request + type: http + seq: 1 +} + +get { + url: {{host}} +} diff --git a/tests/environments/export-environment/global-env-export/global-env-export.spec.ts b/tests/environments/export-environment/global-env-export/global-env-export.spec.ts new file mode 100644 index 000000000..ec0eab2bd --- /dev/null +++ b/tests/environments/export-environment/global-env-export/global-env-export.spec.ts @@ -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(); + }); + }); +}); diff --git a/tests/environments/export-environment/global-env-export/init-user-data/collection-security.json b/tests/environments/export-environment/global-env-export/init-user-data/collection-security.json new file mode 100644 index 000000000..111c05dab --- /dev/null +++ b/tests/environments/export-environment/global-env-export/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/export-environment/global-env-export/init-user-data/global-environments.json b/tests/environments/export-environment/global-env-export/init-user-data/global-environments.json new file mode 100644 index 000000000..d45fa4c26 --- /dev/null +++ b/tests/environments/export-environment/global-env-export/init-user-data/global-environments.json @@ -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" +} diff --git a/tests/environments/export-environment/global-env-export/init-user-data/preferences.json b/tests/environments/export-environment/global-env-export/init-user-data/preferences.json new file mode 100644 index 000000000..5dae54e3b --- /dev/null +++ b/tests/environments/export-environment/global-env-export/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection" + ] +} diff --git a/tests/environments/fixtures/environment-exports/bruno-collection-environments.json b/tests/environments/fixtures/environment-exports/bruno-collection-environments.json new file mode 100644 index 000000000..fbd41dd27 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-collection-environments.json @@ -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 + } + ] + } + ] +} diff --git a/tests/environments/fixtures/environment-exports/bruno-collection-environments/local.json b/tests/environments/fixtures/environment-exports/bruno-collection-environments/local.json new file mode 100644 index 000000000..af3df6bc8 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-collection-environments/local.json @@ -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" + } +} diff --git a/tests/environments/fixtures/environment-exports/bruno-collection-environments/prod.json b/tests/environments/fixtures/environment-exports/bruno-collection-environments/prod.json new file mode 100644 index 000000000..0a60aaa43 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-collection-environments/prod.json @@ -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" + } +} diff --git a/tests/environments/fixtures/environment-exports/bruno-global-environments.json b/tests/environments/fixtures/environment-exports/bruno-global-environments.json new file mode 100644 index 000000000..fbd41dd27 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-global-environments.json @@ -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 + } + ] + } + ] +} diff --git a/tests/environments/fixtures/environment-exports/bruno-global-environments/local.json b/tests/environments/fixtures/environment-exports/bruno-global-environments/local.json new file mode 100644 index 000000000..af3df6bc8 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-global-environments/local.json @@ -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" + } +} diff --git a/tests/environments/fixtures/environment-exports/bruno-global-environments/prod.json b/tests/environments/fixtures/environment-exports/bruno-global-environments/prod.json new file mode 100644 index 000000000..0a60aaa43 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/bruno-global-environments/prod.json @@ -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" + } +} diff --git a/tests/environments/fixtures/environment-exports/local.json b/tests/environments/fixtures/environment-exports/local.json new file mode 100644 index 000000000..af3df6bc8 --- /dev/null +++ b/tests/environments/fixtures/environment-exports/local.json @@ -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" + } +} diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts new file mode 100644 index 000000000..8dda2f31a --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts @@ -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 }); + } + }); + }); +}); diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/bruno.json b/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/bruno.json new file mode 100644 index 000000000..159e1ad08 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Environment Import Test Collection", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/test-request.bru b/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/test-request.bru new file mode 100644 index 000000000..69f9639d1 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/test-request.bru @@ -0,0 +1,9 @@ +meta { + name: Test Request + type: http + seq: 1 +} + +get { + url: {{host}} +} diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/collection-security.json b/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/collection-security.json new file mode 100644 index 000000000..b5962628f --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/preferences.json b/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/preferences.json new file mode 100644 index 000000000..3f481c3aa --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection" + ] +} diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/bruno.json b/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/bruno.json new file mode 100644 index 000000000..159e1ad08 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Environment Import Test Collection", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/test-request.bru b/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/test-request.bru new file mode 100644 index 000000000..69f9639d1 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/test-request.bru @@ -0,0 +1,9 @@ +meta { + name: Test Request + type: http + seq: 1 +} + +get { + url: {{host}} +} diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts b/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts new file mode 100644 index 000000000..39aebc79e --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts @@ -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(); + }); + }); +}); diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/collection-security.json b/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/collection-security.json new file mode 100644 index 000000000..3486270f9 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/preferences.json b/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/preferences.json new file mode 100644 index 000000000..12f2548a8 --- /dev/null +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection" + ] +} diff --git a/tests/environments/import-environment/collection-env-import.spec.ts b/tests/environments/import-environment/collection-env-import.spec.ts index 312dee58f..7eb74a9aa 100644 --- a/tests/environments/import-environment/collection-env-import.spec.ts +++ b/tests/environments/import-environment/collection-env-import.spec.ts @@ -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); diff --git a/tests/environments/import-environment/global-env-import.spec.ts b/tests/environments/import-environment/global-env-import.spec.ts index 9df911278..2fb3a4770 100644 --- a/tests/environments/import-environment/global-env-import.spec.ts +++ b/tests/environments/import-environment/global-env-import.spec.ts @@ -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);