revamp: collection and global env selector dropdown (#5542)

* revamp: collection and global env selector dropdown

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
Co-authored-by: sanish-bruno <sanish@usebruno.com>
Co-authored-by: bernborgess <bernborgesse@outlook.com>
Co-authored-by: lohit <lohit@usebruno.com>
Co-authored-by: Its-Treason <39559178+Its-treason@users.noreply.github.com>
Co-authored-by: jayakrishnancn <jayakrishnancn@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Pooja
2025-09-17 19:35:00 +05:30
committed by GitHub
parent fb2ca8937e
commit 65e69e77b3
36 changed files with 1214 additions and 243 deletions

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { IconPlus, IconDownload, IconSettings } from '@tabler/icons';
const EnvironmentListContent = ({
environments,
activeEnvironmentUid,
description,
onEnvironmentSelect,
onSettingsClick,
onCreateClick,
onImportClick
}) => {
return (
<div>
{environments && environments.length > 0 ? (
<>
<div className="environment-list">
<div className="dropdown-item no-environment" onClick={() => onEnvironmentSelect(null)}>
<span>No Environment</span>
</div>
<div className="pb-[2.625rem]">
{environments.map((env) => (
<div
key={env.uid}
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
onClick={() => onEnvironmentSelect(env)}
>
<span className="max-w-32 truncate no-wrap">{env.name}</span>
</div>
))}
</div>
<div className="dropdown-item configure-button">
<button onClick={onSettingsClick} id="configure-env">
<IconSettings size={16} strokeWidth={1.5} />
<span>Configure</span>
</button>
</div>
</div>
</>
) : (
<div className="empty-state">
<h3>Ready to get started?</h3>
<p>{description}</p>
<div className="space-y-2">
<button onClick={onCreateClick} id="create-env">
<IconPlus size={16} strokeWidth={1.5} />
Create
</button>
<button onClick={onImportClick} id="import-env">
<IconDownload size={16} strokeWidth={1.5} />
Import
</button>
</div>
</div>
)}
</div>
);
};
export default EnvironmentListContent;

View File

@@ -2,14 +2,204 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.current-environment {
background-color: ${(props) => props.theme.sidebar.badge.bg};
border-radius: 15px;
border-radius: 0.9375rem;
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
user-select: none;
background-color: transparent;
border: 1px solid ${(props) => props.theme.dropdown.selectedColor};
line-height: 1rem;
.caret {
margin-left: 0.25rem;
color: rgb(140, 140, 140);
fill: rgb(140, 140, 140);
}
.env-icon {
margin-right: 0.25rem;
color: ${(props) => props.theme.dropdown.selectedColor};
}
.env-text {
color: ${(props) => props.theme.dropdown.selectedColor};
font-size: 0.875rem;
}
.env-separator {
color: #8c8c8c;
margin: 0 0.25rem;
opacity: 0.7;
}
.env-text-inactive {
color: ${(props) => props.theme.dropdown.color};
font-size: 0.875rem;
opacity: 0.7;
}
&.no-environments {
background-color: ${(props) => props.theme.sidebar.badge.bg};
border: 1px solid transparent;
color: ${(props) => props.theme.dropdown.secondaryText};
}
}
.tippy-box {
min-width: 11.875rem;
min-height: 15.0625rem;
font-size: 0.8125rem;
position: relative;
}
.tippy-box .tippy-content {
padding: 0;
.dropdown-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
cursor: pointer;
font-size: 0.8125rem;
color: ${(props) => props.theme.dropdown.primaryText};
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.active {
background-color: ${(props) => props.theme.dropdown.selectedBg};
color: ${(props) => props.theme.dropdown.selectedColor};
}
&.no-environment {
color: ${(props) => props.theme.dropdown.mutedText};
}
}
}
.configure-button {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: ${(props) => props.theme.dropdown.bg};
border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator};
z-index: 10;
margin: 0;
button {
color: ${(props) => props.theme.dropdown.primaryText};
display: flex;
align-items: center;
justify-content: center;
width: 100%;
gap: 0.5rem;
}
}
.tab-button {
color: var(--color-tab-inactive);
font-size: 0.8125rem;
.tab-content-wrapper {
position: relative;
display: flex;
align-items: center;
gap: 0.125rem;
}
&.active {
color: ${(props) => props.theme.tabs.active.color};
border-bottom-color: ${(props) => props.theme.tabs.active.border};
}
&.inactive {
border-bottom-color: transparent;
}
}
.dropdown-item-list {
max-height: 75vh;
overflow-y: scroll;
}
.empty-state {
max-width: 20rem;
margin: 0 auto;
padding: 0.35rem 0.6rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 12.5rem;
h3 {
color: ${(props) => props.theme.dropdown.primaryText};
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
line-height: 1.4;
}
p {
color: ${(props) => props.theme.dropdown.primaryText};
opacity: 0.75;
font-size: 0.6875rem;
line-height: 1.5;
margin-bottom: 1rem;
max-width: 11.875rem;
margin: 0 auto;
margin-bottom: 1rem;
}
.space-y-2 {
width: 100%;
align-self: stretch;
}
.space-y-2 > button {
border: 0.0625rem solid ${(props) => props.theme.dropdown.primaryText};
background: transparent;
color: ${(props) => props.theme.dropdown.primaryText};
padding: 0.5rem 1rem;
border-radius: 0.375rem;
width: 100%;
margin-bottom: 0.5rem;
font-size: 0.75rem;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
&:hover {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:last-child {
margin-bottom: 0;
}
}
}
.no-collection-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
color: ${(props) => props.theme.dropdown.primaryText};
font-size: 0.8125rem;
line-height: 1.5;
text-align: center;
opacity: 0.75;
svg {
margin: 0 auto 1rem auto;
color: ${(props) => props.theme.dropdown.primaryText};
opacity: 0.5;
}
}
`;

View File

@@ -1,95 +1,240 @@
import React, { useRef, forwardRef, useState } from 'react';
import React, { useState, useRef, forwardRef } from 'react';
import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
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 CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
import ImportGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment';
import StyledWrapper from './StyledWrapper';
const EnvironmentSelector = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const { environments, activeEnvironmentUid } = collection;
const activeEnvironment = activeEnvironmentUid ? find(environments, (e) => e.uid === activeEnvironmentUid) : null;
const [activeTab, setActiveTab] = useState('collection');
const [showGlobalSettings, setShowGlobalSettings] = useState(false);
const [showCollectionSettings, setShowCollectionSettings] = useState(false);
const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);
const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);
const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);
const [showImportCollectionModal, setShowImportCollectionModal] = useState(false);
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
const activeGlobalEnvironment = activeGlobalEnvironmentUid
? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)
: null;
const environments = collection?.environments || [];
const activeEnvironmentUid = collection?.activeEnvironmentUid;
const activeCollectionEnvironment = activeEnvironmentUid
? find(environments, (e) => e.uid === activeEnvironmentUid)
: null;
const tabs = [
{ id: 'collection', label: 'Collection', icon: <IconDatabase size={16} strokeWidth={1.5} /> },
{ id: 'global', label: 'Global', icon: <IconWorld size={16} strokeWidth={1.5} /> }
];
const onDropdownCreate = (ref) => {
dropdownTippyRef.current = ref;
};
// Get description based on active tab
const description =
activeTab === 'collection'
? 'Create your first environment to begin working with your collection.'
: 'Create your first global environment to begin working across collections.';
// Environment selection handler
const handleEnvironmentSelect = (environment) => {
const action =
activeTab === 'collection'
? selectEnvironment(environment ? environment.uid : null, collection.uid)
: selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null });
dispatch(action)
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
} else {
toast.success('No Environments are active now');
}
dropdownTippyRef.current.hide();
})
.catch((err) => {
toast.error('An error occurred while selecting the environment');
});
};
// Settings handler
const handleSettingsClick = () => {
if (activeTab === 'collection') {
dispatch(updateEnvironmentSettingsModalVisibility(true));
setShowCollectionSettings(true);
} else {
setShowGlobalSettings(true);
}
dropdownTippyRef.current.hide();
};
// Create handler
const handleCreateClick = () => {
if (activeTab === 'collection') {
setShowCreateCollectionModal(true);
} else {
setShowCreateGlobalModal(true);
}
dropdownTippyRef.current.hide();
};
// Import handler
const handleImportClick = () => {
if (activeTab === 'collection') {
setShowImportCollectionModal(true);
} else {
setShowImportGlobalModal(true);
}
dropdownTippyRef.current.hide();
};
// Modal handlers
const handleCloseSettings = () => {
setShowGlobalSettings(false);
setShowCollectionSettings(false);
dispatch(updateEnvironmentSettingsModalVisibility(false));
};
// Create icon component for dropdown trigger
const Icon = forwardRef((props, ref) => {
const hasAnyEnv = activeGlobalEnvironment || activeCollectionEnvironment;
const displayContent = hasAnyEnv ? (
<>
{activeCollectionEnvironment && (
<>
<div className="flex items-center">
<IconDatabase size={14} strokeWidth={1.5} className="env-icon" />
<span className="env-text max-w-24 truncate no-wrap">{activeCollectionEnvironment.name}</span>
</div>
{activeGlobalEnvironment && <span className="env-separator">|</span>}
</>
)}
{activeGlobalEnvironment && (
<div className="flex items-center">
<IconWorld size={14} strokeWidth={1.5} className="env-icon" />
<span className="env-text max-w-24 truncate no-wrap">{activeGlobalEnvironment.name}</span>
</div>
)}
</>
) : (
<span className="env-text-inactive">No environments</span>
);
return (
<div ref={ref} className="current-environment collection-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
<p className="text-nowrap truncate max-w-32" title={activeEnvironment ? activeEnvironment.name : 'No Environment'}>{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<div
ref={ref}
className={`current-environment flex align-center justify-center cursor-pointer bg-transparent ${
!hasAnyEnv ? 'no-environments' : ''
}`}
data-testid="environment-selector-trigger"
>
{displayContent}
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);
});
const handleSettingsIconClick = () => {
setOpenSettingsModal(true);
dispatch(updateEnvironmentSettingsModalVisibility(true));
};
const handleModalClose = () => {
setOpenSettingsModal(false);
dispatch(updateEnvironmentSettingsModalVisibility(false));
};
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const onSelect = (environment) => {
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
} else {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
};
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer environment-selector">
<div className="environment-selector flex align-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div className="label-item font-medium">Collection Environments</div>
{environments && environments.length
? environments.map((e) => (
<div
className={`dropdown-item ${e?.uid === activeEnvironmentUid ? 'active' : ''}`}
key={e.uid}
onClick={() => {
onSelect(e);
dropdownTippyRef.current.hide();
}}
>
<IconDatabase size={18} strokeWidth={1.5} /> <span className="ml-2 break-all">{e.name}</span>
</div>
))
: null}
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onSelect(null);
}}
>
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
{/* Tab Headers */}
<div className="tab-header flex justify-center p-[0.75rem]">
{tabs.map((tab) => (
<button
key={tab.id}
className={`tab-button whitespace-nowrap pb-[0.375rem] border-b-[0.125rem] bg-transparent flex align-center cursor-pointer transition-all duration-200 mr-[1.25rem] ${
activeTab === tab.id ? 'active' : 'inactive'
}`}
onClick={() => setActiveTab(tab.id)}
data-testid={`env-tab-${tab.id}`}
>
<span className="tab-content-wrapper">
{tab.icon}
{tab.label}
</span>
</button>
))}
</div>
<div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600" id="Configure">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Configure</span>
{/* Tab Content */}
<div className="tab-content">
<EnvironmentListContent
environments={activeTab === 'collection' ? environments : globalEnvironments}
activeEnvironmentUid={activeTab === 'collection' ? activeEnvironmentUid : activeGlobalEnvironmentUid}
description={description}
onEnvironmentSelect={handleEnvironmentSelect}
onSettingsClick={handleSettingsClick}
onCreateClick={handleCreateClick}
onImportClick={handleImportClick}
/>
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={handleModalClose} />}
{/* Modals - Rendered outside dropdown to avoid conflicts */}
{showGlobalSettings && (
<GlobalEnvironmentSettings globalEnvironments={globalEnvironments} onClose={handleCloseSettings} />
)}
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
{showCreateGlobalModal && (
<CreateGlobalEnvironment
onClose={() => setShowCreateGlobalModal(false)}
onEnvironmentCreated={() => {
setShowGlobalSettings(true);
}}
/>
)}
{showImportGlobalModal && (
<ImportGlobalEnvironment
onClose={() => setShowImportGlobalModal(false)}
onEnvironmentCreated={() => {
setShowGlobalSettings(true);
}}
/>
)}
{showCreateCollectionModal && (
<CreateEnvironment
collection={collection}
onClose={() => setShowCreateCollectionModal(false)}
onEnvironmentCreated={() => {
setShowCollectionSettings(true);
}}
/>
)}
{showImportCollectionModal && (
<ImportEnvironment
collection={collection}
onClose={() => setShowImportCollectionModal(false)}
onEnvironmentCreated={() => {
setShowCollectionSettings(true);
}}
/>
)}
</StyledWrapper>
);
};

View File

@@ -8,7 +8,7 @@ import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const inputRef = useRef();
@@ -37,6 +37,10 @@ const CreateEnvironment = ({ collection, onClose }) => {
.then(() => {
toast.success('Environment created in collection');
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch(() => toast.error('An error occurred while creating the environment'));
}

View File

@@ -254,6 +254,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
onClick={addVariable}
id="add-variable"
data-testid="add-variable"
>
+ Add Variable
</button>
@@ -261,15 +262,15 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</div>
<div className="flex items-center">
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit}>
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit} data-testid="save-env">
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
Save
</button>
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset}>
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset} data-testid="reset-env">
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
Reset
</button>
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate}>
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate} data-testid="activate-env">
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
Activate
</button>

View File

@@ -8,7 +8,7 @@ import { importEnvironment } from 'providers/ReduxStore/slices/collections/actio
import { toastError } from 'utils/common/error';
import { IconDatabaseImport } from '@tabler/icons';
const ImportEnvironment = ({ collection, onClose }) => {
const ImportEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
@@ -36,17 +36,22 @@ const ImportEnvironment = ({ collection, onClose }) => {
})
.then(() => {
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
return (
<Portal>
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-environment-modal">
<button
type="button"
onClick={handleImportPostmanEnvironment}
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
data-testid="import-postman-environment"
>
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>

View File

@@ -56,9 +56,8 @@ const EnvironmentSettings = ({ collection, onClose }) => {
) : tab === 'import' ? (
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
) : (
<></>
<DefaultTab setTab={setTab} />
)}
<DefaultTab setTab={setTab} />
</Modal>
</StyledWrapper>
);

View File

@@ -1,18 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.current-environment {
}
.environment-active {
padding: 0.3rem 0.4rem;
color: ${(props) => props.theme.colors.text.yellow};
border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
}
.environment-selector {
.active: {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`;
export default Wrapper;

View File

@@ -1,100 +0,0 @@
import React, { useRef, forwardRef, useState } from 'react';
import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { IconSettings, IconWorld, IconDatabase, IconDatabaseOff, IconCheck } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import ToolHint from 'components/ToolHint/index';
const EnvironmentSelector = () => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const activeEnvironment = activeGlobalEnvironmentUid ? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid) : null;
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className={`current-environment global-environment flex flex-row gap-1 rounded-xl text-xs cursor-pointer items-center justify-center select-none ${activeGlobalEnvironmentUid? 'environment-active': ''}`}>
<ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId" className='flex flex-row'>
<IconWorld className="globe" size={16} strokeWidth={1.5} />
{
activeEnvironment ? <div className='text-nowrap truncate max-w-32'>{activeEnvironment?.name}</div> : null
}
</ToolHint>
</div>
);
});
const handleSettingsIconClick = () => {
setOpenSettingsModal(true);
};
const handleModalClose = () => {
setOpenSettingsModal(false);
};
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const onSelect = (environment) => {
dispatch(selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null }))
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
} else {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
};
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer environment-selector mr-3">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end" transparent={true}>
<div className="label-item font-medium">Global Environments</div>
{globalEnvironments && globalEnvironments.length
? globalEnvironments.map((e) => (
<div
className={`dropdown-item ${e?.uid === activeGlobalEnvironmentUid ? 'active' : ''}`}
key={e.uid}
onClick={() => {
onSelect(e);
dropdownTippyRef.current.hide();
}}
>
<IconDatabase size={18} strokeWidth={1.5} /> <span className="ml-2 break-all">{e.name}</span>
</div>
))
: null}
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onSelect(null);
}}
>
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Configure</span>
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings globalEnvironments={globalEnvironments} activeGlobalEnvironmentUid={activeGlobalEnvironmentUid} onClose={handleModalClose} />}
</StyledWrapper>
);
};
export default EnvironmentSelector;

View File

@@ -8,7 +8,7 @@ import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ onClose }) => {
const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
const validateEnvironmentName = (name) => {
@@ -39,6 +39,10 @@ const CreateEnvironment = ({ onClose }) => {
.then(() => {
toast.success('Global environment created!');
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch(() => toast.error('An error occurred while creating the environment'));
}

View File

@@ -179,6 +179,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
ref={addButtonRef}
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
onClick={addVariable}
data-testid="add-variable"
>
+ Add Variable
</button>
@@ -186,10 +187,10 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</div>
<div>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit} data-testid="save-env">
Save
</button>
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset}>
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset} data-testid="reset-env">
Reset
</button>
</div>

View File

@@ -9,7 +9,7 @@ import { IconDatabaseImport } from '@tabler/icons';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { uuid } from 'utils/common/index';
const ImportEnvironment = ({ onClose }) => {
const ImportEnvironment = ({ onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
@@ -37,17 +37,22 @@ const ImportEnvironment = ({ onClose }) => {
})
.then(() => {
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
return (
<Portal>
<Modal size="sm" title="Import Global Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<Modal size="sm" title="Import Global Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-global-environment-modal">
<button
type="button"
onClick={handleImportPostmanEnvironment}
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
data-testid="import-postman-global-environment"
>
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>

View File

@@ -39,7 +39,7 @@ const DefaultTab = ({ setTab }) => {
);
};
const EnvironmentSettings = ({ globalEnvironments, activeGlobalEnvironmentUid, onClose }) => {
const EnvironmentSettings = ({ globalEnvironments, onClose }) => {
const [isModified, setIsModified] = useState(false);
const environments = globalEnvironments;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
@@ -53,9 +53,8 @@ const EnvironmentSettings = ({ globalEnvironments, activeGlobalEnvironmentUid, o
) : tab === 'import' ? (
<ImportEnvironment onClose={() => setTab('default')} />
) : (
<></>
<DefaultTab setTab={setTab} />
)}
<DefaultTab setTab={setTab} />
</Modal>
</StyledWrapper>
);
@@ -65,7 +64,6 @@ const EnvironmentSettings = ({ globalEnvironments, activeGlobalEnvironmentUid, o
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList
environments={globalEnvironments}
activeEnvironmentUid={activeGlobalEnvironmentUid}
selectedEnvironment={selectedEnvironment}
setSelectedEnvironment={setSelectedEnvironment}
isModified={isModified}

View File

@@ -71,7 +71,8 @@ const Modal = ({
disableCloseOnOutsideClick,
disableEscapeKey,
onClick,
closeModalFadeTimeout = 500
closeModalFadeTimeout = 500,
dataTestId
}) => {
const modalRef = useRef(null);
const [isClosing, setIsClosing] = useState(false);
@@ -120,6 +121,7 @@ const Modal = ({
role="dialog"
aria-labelledby="modal-title"
aria-describedby="modal-description"
data-testid={dataTestId}
>
<ModalHeader
title={title}

View File

@@ -147,7 +147,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
</div>
</div>
{generateCodeItemModalOpen && (

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import GlobalEnvironmentSelector from 'components/GlobalEnvironments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import ToolHint from 'components/ToolHint';
@@ -69,9 +68,8 @@ const CollectionToolBar = ({ collection }) => {
</ToolHint>
</span>
<span>
<GlobalEnvironmentSelector />
<EnvironmentSelector collection={collection} />
</span>
<EnvironmentSelector collection={collection} />
</div>
</div>
</StyledWrapper>

View File

@@ -16,7 +16,7 @@ const StatusCode = ({ status }) => {
};
return (
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`}>
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`} data-testid="response-status-code">
{status} {statusCodePhraseMap[status]}
</StyledWrapper>
);

View File

@@ -153,7 +153,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
]
return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose} dataTestId="import-collection-modal">
<div className="flex flex-col">
<div className="mb-4">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Import from file</h3>

View File

@@ -48,7 +48,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
const onSubmit = () => formik.handleSubmit();
return (
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose} dataTestId="import-collection-location-modal">
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<div>
<label htmlFor="collectionName" className="block font-semibold">

View File

@@ -94,6 +94,7 @@ export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, get
ipcRenderer
.invoke('renderer:create-global-environment', { name, uid, variables })
.then(() => dispatch(_addGlobalEnvironment({ name, uid, variables })))
.then(() => dispatch(_selectGlobalEnvironment({ environmentUid: uid })))
.then(resolve)
.catch(reject);
});

View File

@@ -79,10 +79,16 @@ const darkTheme = {
color: 'rgb(204, 204, 204)',
iconColor: 'rgb(204, 204, 204)',
bg: 'rgb(48, 48, 49)',
hoverBg: '#185387',
hoverBg: '#6A6A6A29',
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
separator: '#444',
labelBg: '#4a4949'
labelBg: '#4a4949',
selectedBg: '#F59E0B14',
selectedColor: '#F59E0B',
mutedText: '#9B9B9B',
primaryText: '#D4D4D4',
secondaryText: '#9CA3AF',
headingText: '#FFFFFF'
},
request: {
@@ -226,8 +232,8 @@ const darkTheme = {
tabs: {
active: {
color: '#ccc',
border: '#569cd6'
color: '#CCCCCC',
border: '#F59E0B'
}
},

View File

@@ -79,10 +79,16 @@ const lightTheme = {
color: 'rgb(48 48 48)',
iconColor: 'rgb(75, 85, 99)',
bg: '#fff',
hoverBg: '#e9e9e9',
hoverBg: '#e9ecef',
shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',
separator: '#e7e7e7',
labelBg: '#f3f3f3'
labelBg: '#f3f3f3',
selectedBg: '#D977060F',
selectedColor: '#D97706',
mutedText: '#9B9B9B',
primaryText: '#343434',
secondaryText: '#6B7280',
headingText: '#343434'
},
request: {
@@ -227,8 +233,8 @@ const lightTheme = {
tabs: {
active: {
color: 'rgb(50, 46, 44)',
border: '#546de5'
color: '#343434',
border: '#D97706'
}
},

View File

@@ -15,7 +15,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
await page.getByText('api-setEnvVar-with-persist', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
await page.locator('div.current-environment').click();
// select stage environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Stage' })).toBeVisible();
@@ -27,7 +27,8 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
await page.waitForTimeout(1000);
// confirm that the environment variable is set
await page.getByTitle('Stage', { exact: true }).click();
await page.locator('div.current-environment').click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
@@ -42,7 +43,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
await newPage.getByText('api-setEnvVar-with-persist', { exact: true }).click();
// open environment dropdown
await newPage.locator('div.current-environment.collection-environment').click();
await newPage.locator('div.current-environment').click();
await newPage.getByText('Configure', { exact: true }).click();
await expect(newPage.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();

View File

@@ -9,7 +9,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
await page.getByText('api-setEnvVar-without-persist', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
await page.locator('div.current-environment').click();
// select stage environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Stage' })).toBeVisible();
@@ -21,7 +21,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
await page.waitForTimeout(1000);
// confirm that the environment variable is set
await page.getByTitle('Stage', { exact: true }).click();
await page.locator('div.current-environment').click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
@@ -36,7 +36,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
await newPage.getByText('api-setEnvVar-without-persist', { exact: true }).click();
// open environment dropdown
await newPage.locator('div.current-environment.collection-environment').click();
await newPage.locator('div.current-environment').click();
await newPage.getByText('Configure', { exact: true }).click();
// ensure that the environment variable is not persisted

View File

@@ -0,0 +1,127 @@
import { test, expect } from '../../../playwright';
import path from 'path';
test.describe('Collection Environment Create Tests', () => {
test('should import collection and create environment for request usage', async ({
pageWithUserData: page,
createTmpDir
}) => {
const openApiFile = path.join(__dirname, 'fixtures', 'bruno-collection.json');
// Import test collection
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');
await importModal.waitFor({ state: 'visible' });
await page.setInputFiles('input[type="file"]', openApiFile);
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await expect(locationModal.getByText('test_collection')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('env-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' })).toBeVisible();
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create environment
await page.locator('[data-testid="environment-selector-trigger"]').click();
await expect(page.locator('[data-testid="env-tab-collection"]')).toHaveClass(/active/);
// Create new environment
await page.locator('button[id="create-env"]').click();
// Fill environment name
const environmentNameInput = page.locator('input[name="name"]');
await expect(environmentNameInput).toBeVisible();
await environmentNameInput.fill('Test Environment');
await page.getByRole('button', { name: 'Create' }).click();
// Add environment variables
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="0.name"]').fill('host');
await page
.locator('tr')
.filter({ has: page.locator('input[name="0.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('https://echo.usebruno.com');
// Add userId
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="1.name"]').fill('userId');
await page
.locator('tr')
.filter({ has: page.locator('input[name="1.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('1');
// Add postTitle
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="2.name"]').fill('postTitle');
await page
.locator('tr')
.filter({ has: page.locator('input[name="2.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('Test Post from Environment');
// Add postBody
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="3.name"]').fill('postBody');
await page
.locator('tr')
.filter({ has: page.locator('input[name="3.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('This is a test post body with environment variables');
// Add secret token
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="4.name"]').fill('secretApiToken');
await page
.locator('tr')
.filter({ has: page.locator('input[name="4.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('super-secret-token-12345');
await page.locator('input[name="4.secret"]').check();
// Save environment
await page.getByRole('button', { name: 'Save' }).click();
await page.getByText('×').click();
await expect(page.locator('.current-environment')).toContainText('Test Environment');
// Test GET request with environment variables
await page.locator('.collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200');
// Verify the JSON response contains the environment variables
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('"userId": 1');
await expect(responsePane).toContainText('"title": "Test Post from Environment"');
await expect(responsePane).toContainText('"body": "This is a test post body with environment variables"');
await expect(responsePane).toContainText('"apiToken": "super-secret-token-12345"');
// Cleanup
await page
.locator('.collection-name')
.filter({ has: page.locator('#sidebar-collection-name:has-text("test_collection")') })
.locator('.collection-actions')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('.bruno-logo').click();
});
});

View File

@@ -0,0 +1,61 @@
{
"name": "test_collection",
"version": "1",
"items": [
{
"type": "http",
"name": "test",
"filename": "test.bru",
"seq": 1,
"settings": {
"encodeUrl": true
},
"tags": [],
"request": {
"url": "{{host}}",
"method": "POST",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"userId\": {{userId}},\n \"title\": \"{{postTitle}}\",\n \"body\": \"{{postBody}}\",\n \"apiToken\": \"{{secretApiToken}}\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
}
],
"environments": [],
"brunoConfig": {
"version": "1",
"name": "test_collection",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"size": 0.000133514404296875,
"filesCount": 1,
"proxy": {
"bypassProxy": "",
"enabled": false,
"auth": {
"enabled": false,
"username": "",
"password": ""
},
"port": null,
"hostname": "",
"protocol": "http"
}
}
}

View File

@@ -0,0 +1,130 @@
import { test, expect } from '../../../playwright';
import path from 'path';
test.describe('Global Environment Create Tests', () => {
test('should import collection and create global environment for request usage', async ({
pageWithUserData: page,
createTmpDir
}) => {
const openApiFile = path.join(__dirname, 'fixtures', 'bruno-collection.json');
// Import test collection
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');
await importModal.waitFor({ state: 'visible' });
await page.setInputFiles('input[type="file"]', openApiFile);
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await expect(locationModal.getByText('test_collection')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('global-env-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' })).toBeVisible();
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create 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/);
// Create new global environment
await page.locator('button[id="create-env"]').click();
// Fill environment name
const environmentNameInput = page.locator('input[name="name"]');
await expect(environmentNameInput).toBeVisible();
await environmentNameInput.fill('Test Global Environment');
await page.getByRole('button', { name: 'Create' }).click();
// Add environment variables
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="0.name"]').fill('host');
await page
.locator('tr')
.filter({ has: page.locator('input[name="0.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('https://echo.usebruno.com');
// Add userId
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="1.name"]').fill('userId');
await page
.locator('tr')
.filter({ has: page.locator('input[name="1.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('1');
// Add postTitle
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="2.name"]').fill('postTitle');
await page
.locator('tr')
.filter({ has: page.locator('input[name="2.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('Global Test Post from Environment');
// Add postBody
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="3.name"]').fill('postBody');
await page
.locator('tr')
.filter({ has: page.locator('input[name="3.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('This is a global test post body with environment variables');
// Add secret token
await page.locator('button[data-testid="add-variable"]').click();
await page.locator('input[name="4.name"]').fill('secretApiToken');
await page
.locator('tr')
.filter({ has: page.locator('input[name="4.name"]') })
.locator('.CodeMirror')
.click();
await page.keyboard.type('global-secret-token-12345');
await page.locator('input[name="4.secret"]').check();
await expect(page.locator('input[name="4.secret"]')).toBeChecked();
// Save environment
await page.getByRole('button', { name: 'Save' }).click();
await page.getByText('×').click();
await expect(page.locator('.current-environment')).toContainText('Test Global Environment');
// Test GET request with environment variables
await page.locator('.collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200');
// Verify the JSON response contains the environment variables
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('"userId": 1');
await expect(responsePane).toContainText('"title": "Global Test Post from Environment"');
await expect(responsePane).toContainText('"body": "This is a global test post body with environment variables"');
await expect(responsePane).toContainText('"apiToken": "global-secret-token-12345"');
// Cleanup
await page
.locator('.collection-name')
.filter({ has: page.locator('#sidebar-collection-name:has-text("test_collection")') })
.locator('.collection-actions')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('.bruno-logo').click();
});
});

View File

@@ -0,0 +1,94 @@
import { test, expect } from '../../../playwright';
import path from 'path';
test.describe('Collection Environment Import Tests', () => {
test('should import collection environment from file', async ({ pageWithUserData: page, createTmpDir }) => {
const openApiFile = path.join(__dirname, 'fixtures', 'collection.json');
const envFile = path.join(__dirname, 'fixtures', 'collection-env.json');
// Import test collection
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');
await importModal.waitFor({ state: 'visible' });
await page.setInputFiles('input[type="file"]', openApiFile);
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await expect(locationModal.getByText('Environment Test Collection')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('collection-env-import-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(
page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })
).toBeVisible();
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Import collection environment
await page.locator('[data-testid="environment-selector-trigger"]').click();
await expect(page.locator('[data-testid="env-tab-collection"]')).toHaveClass(/active/);
await page.locator('button[id="import-env"]').click();
const importEnvModal = page.locator('[data-testid="import-environment-modal"]');
await expect(importEnvModal).toBeVisible();
// Import environment file
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('button[data-testid="import-postman-environment"]').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(envFile);
// Wait for import to complete and environment settings modal to open
await expect(page.locator('.current-environment')).toContainText('Test Collection Environment');
// The environment settings modal should now be visible with the imported environment
const envSettingsModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
await expect(envSettingsModal).toBeVisible();
// Verify imported variables in Test Collection Environment settings
await expect(envSettingsModal.locator('input[name="0.name"]')).toHaveValue('host');
await expect(envSettingsModal.locator('input[name="1.name"]')).toHaveValue('userId');
await expect(envSettingsModal.locator('input[name="2.name"]')).toHaveValue('apiKey');
await expect(envSettingsModal.locator('input[name="3.name"]')).toHaveValue('postTitle');
await expect(envSettingsModal.locator('input[name="4.name"]')).toHaveValue('postBody');
await expect(envSettingsModal.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
await expect(envSettingsModal.locator('input[name="5.secret"]')).toBeChecked();
await page.getByText('×').click();
// Test GET request with imported environment
await page.locator('.collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200');
// Verify the JSON response contains the interpolated userId
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('"userId": 1');
// Test POST request
await page.locator('.collection-item-name').nth(1).click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('201');
// Cleanup
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page
.locator('.collection-name')
.filter({ has: page.locator('#sidebar-collection-name:has-text("Environment Test Collection")') })
.locator('.collection-actions')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('.bruno-logo').click();
});
});

View File

@@ -0,0 +1,45 @@
{
"id": "test-collection-env-id",
"name": "Test Collection Environment",
"values": [
{
"key": "host",
"value": "https://jsonplaceholder.typicode.com",
"enabled": true,
"type": "text"
},
{
"key": "userId",
"value": "1",
"enabled": true,
"type": "text"
},
{
"key": "apiKey",
"value": "collection-api-key-12345",
"enabled": true,
"type": "text"
},
{
"key": "postTitle",
"value": "Collection Environment Test Post",
"enabled": true,
"type": "text"
},
{
"key": "postBody",
"value": "This is a test post created using collection environment variables",
"enabled": true,
"type": "text"
},
{
"key": "secretApiToken",
"value": "collection-secret-token-67890",
"enabled": true,
"type": "secret"
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2024-01-01T00:00:00.000Z",
"_postman_exported_using": "Postman/10.0.0"
}

View File

@@ -0,0 +1,67 @@
{
"info": {
"name": "Environment Test Collection",
"description": "Test collection for environment import and usage tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_postman_id": "env-test-collection-id"
},
"item": [
{
"name": "Get Posts with Environment Variables",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{apiKey}}",
"type": "text"
},
{
"key": "X-Secret-Token",
"value": "{{secretApiToken}}",
"type": "text"
}
],
"url": {
"raw": "{{host}}/posts/{{userId}}",
"host": ["{{host}}"],
"path": ["posts", "{{userId}}"]
}
},
"response": []
},
{
"name": "Create Post with Body Variables",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{apiKey}}",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
},
{
"key": "X-Secret-Token",
"value": "{{secretApiToken}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"title\": \"{{postTitle}}\",\n \"body\": \"{{postBody}}\",\n \"userId\": {{userId}}\n}"
},
"url": {
"raw": "{{host}}/posts",
"host": ["{{host}}"],
"path": ["posts"]
}
},
"response": []
}
]
}

View File

@@ -0,0 +1,45 @@
{
"id": "test-global-env-id",
"name": "Test Global Environment",
"values": [
{
"key": "host",
"value": "https://jsonplaceholder.typicode.com",
"enabled": true,
"type": "text"
},
{
"key": "userId",
"value": "1",
"enabled": true,
"type": "text"
},
{
"key": "apiKey",
"value": "global-api-key-12345",
"enabled": true,
"type": "text"
},
{
"key": "postTitle",
"value": "Global Test Post from Environment",
"enabled": true,
"type": "text"
},
{
"key": "postBody",
"value": "This is a global test post body with environment variables",
"enabled": true,
"type": "text"
},
{
"key": "secretApiToken",
"value": "global-secret-token-67890",
"enabled": true,
"type": "secret"
}
],
"_postman_variable_scope": "globals",
"_postman_exported_at": "2024-01-01T00:00:00.000Z",
"_postman_exported_using": "Postman/10.0.0"
}

View File

@@ -0,0 +1,95 @@
import { test, expect } from '../../../playwright';
import path from 'path';
test.describe('Global Environment Import Tests', () => {
test('should import global environment from file', async ({ pageWithUserData: page, createTmpDir }) => {
const openApiFile = path.join(__dirname, 'fixtures', 'collection.json');
const globalEnvFile = path.join(__dirname, 'fixtures', 'global-env.json');
// Import test collection
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');
await importModal.waitFor({ state: 'visible' });
await page.setInputFiles('input[type="file"]', openApiFile);
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await expect(locationModal.getByText('Environment Test Collection')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('global-env-import-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(
page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })
).toBeVisible();
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page.getByLabel('Safe Mode').check();
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();
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();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(globalEnvFile);
// Wait for import to complete and global environment settings modal to open
await expect(page.locator('.current-environment')).toContainText('Test Global Environment');
// The global environment settings modal should now be visible with the imported environment
const globalEnvSettingsModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
await expect(globalEnvSettingsModal).toBeVisible();
// Verify imported variables in Test Global Environment settings
await expect(globalEnvSettingsModal.locator('input[name="0.name"]')).toHaveValue('host');
await expect(globalEnvSettingsModal.locator('input[name="1.name"]')).toHaveValue('userId');
await expect(globalEnvSettingsModal.locator('input[name="2.name"]')).toHaveValue('apiKey');
await expect(globalEnvSettingsModal.locator('input[name="3.name"]')).toHaveValue('postTitle');
await expect(globalEnvSettingsModal.locator('input[name="4.name"]')).toHaveValue('postBody');
await expect(globalEnvSettingsModal.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
await expect(globalEnvSettingsModal.locator('input[name="5.secret"]')).toBeChecked();
await page.getByText('×').click();
// Test GET request with global environment
await page.locator('.collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200');
// Verify the JSON response contains the interpolated userId
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('"userId": 1');
// Test POST request
await page.locator('.collection-item-name').nth(1).click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('201');
// Cleanup
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page
.locator('.collection-name')
.filter({ has: page.locator('#sidebar-collection-name:has-text("Environment Test Collection")') })
.locator('.collection-actions')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('.bruno-logo').click();
});
});

View File

@@ -13,7 +13,7 @@ test.describe('Multiline Variables - Read Environment Test', () => {
await page.getByTitle('request', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
await page.locator('div.current-environment').click();
// select test environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();

View File

@@ -13,7 +13,7 @@ test.describe('Multiline Variables - Write Test', () => {
await page.getByTitle('multiline-test', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
await page.locator('div.current-environment').click();
// select test environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();
@@ -21,12 +21,11 @@ test.describe('Multiline Variables - Write Test', () => {
await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();
// select configure button from environment dropdown
await expect(page.getByTitle('Test', { exact: true })).toBeVisible();
await page.getByTitle('Test', { exact: true }).click();
await page.locator('div.current-environment').click();
// open environment configuration
await expect(page.locator('#Configure')).toBeVisible();
await page.locator('#Configure').click();
await expect(page.getByText('Configure', { exact: true })).toBeVisible();
await page.getByText('Configure', { exact: true }).click();
// add variable
await page.getByRole('button', { name: /Add.*Variable/i }).click();

View File

@@ -6,22 +6,22 @@ test.describe('Invalid File Handling', () => {
test('Handle invalid file without crashing', async ({ page }) => {
const invalidFile = path.join(testDataDir, 'invalid.txt');
await page.getByRole('button', { name: 'Import Collection' }).click();
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await page.setInputFiles('input[type="file"]', invalidFile);
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const hasError = await page.getByText("Failed to parse the file ensure it is valid JSON or YAML").isVisible();
const hasError = await page.getByText("Failed to parse the file ensure it is valid JSON or YAML").first().isVisible();
expect(hasError).toBe(true);
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});

View File

@@ -8,19 +8,19 @@ test.describe('Invalid Postman Collection - Missing Info', () => {
const postmanFile = path.join(testDataDir, 'postman-invalid-missing-info.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await page.setInputFiles('input[type="file"]', postmanFile);
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
// Check for error message
const hasError = await page.getByText('Import collection failed').isVisible();
const hasError = await page.getByText('Import collection failed').first().isVisible();
expect(hasError).toBe(true);
// Cleanup: close any open modals