fix: resolve process.env variables in global environment level (#7600)

* feat: enhance environment variable resolution in EnvironmentVariablesTable

- Added logic to populate process environment variables from the active workspace when no collection is selected, allowing for proper resolution of {{process.env.X}}.
- Updated workspace actions to map scratch collections to their respective workspaces, ensuring that environment variables can be resolved correctly.

This improves the user experience by providing access to workspace-specific environment variables in the environment variables table.

* refactor: improve state selection and add test IDs for better testing

- Refactored the state selection logic in EnvironmentVariablesTable for clarity.
- Added data-testid attributes to various components including CollapsibleSection, DotEnvFileDetails, DotEnvRawView, and EnvironmentList to enhance testability.
- This change aims to streamline component interactions and facilitate easier testing.

* feat: add test IDs to EnvironmentList for improved testability

- Introduced data-testid attributes to the EnvironmentList component, enhancing the ability to target elements in tests.
- This update includes test IDs for the CollapsibleSection, create .env file button, and the input field for the .env name, facilitating better integration with testing frameworks.

* refactor: simplify global environment test setup

- Removed unnecessary timeout setting and afterEach cleanup logic from the global environment process.env resolution test.
- This change streamlines the test structure, focusing on the core functionality being tested.
This commit is contained in:
Abhishek S Lal
2026-04-04 14:57:50 +05:30
committed by GitHub
parent c273c10f0c
commit fabba4d296
14 changed files with 153 additions and 4 deletions

View File

@@ -45,6 +45,10 @@ const EnvironmentVariablesTable = ({
}) => {
const { storedTheme } = useTheme();
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const activeWorkspace = useSelector((state) => {
const uid = state.workspaces?.activeWorkspaceUid;
return state.workspaces?.workspaces?.find((w) => w.uid === uid);
});
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -138,6 +142,12 @@ const EnvironmentVariablesTable = ({
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
}
// When collection is null (global/workspace environments), populate process env
// variables from the active workspace so that {{process.env.X}} can resolve
if (!collection && activeWorkspace?.processEnvVariables) {
_collection.workspaceProcessEnvVariables = activeWorkspace.processEnvVariables;
}
const initialValues = useMemo(() => {
const vars = environment.variables || [];
return [

View File

@@ -8,11 +8,12 @@ const CollapsibleSection = ({
onToggle,
badge,
actions,
children
children,
testId
}) => {
return (
<StyledWrapper className={expanded ? 'expanded' : 'collapsed'}>
<div className="section-header" onClick={onToggle}>
<div className="section-header" onClick={onToggle} data-testid={testId}>
<div className="section-title-wrapper">
<IconChevronRight
size={14}

View File

@@ -44,6 +44,7 @@ const DotEnvFileDetails = ({
className={`toggle-btn ${viewMode === 'raw' ? 'active' : ''}`}
onClick={() => onViewModeChange?.('raw')}
aria-pressed={viewMode === 'raw'}
data-testid="dotenv-view-raw"
>
Raw
</button>

View File

@@ -13,7 +13,7 @@ const DotEnvRawView = ({
}) => {
return (
<>
<div className="raw-editor-container">
<div className="raw-editor-container" data-testid="dotenv-raw-editor">
<CodeEditor
collection={collection}
item={item}

View File

@@ -46,7 +46,7 @@ const EnvironmentListContent = ({
</div>
</ToolHint>
<div className="dropdown-item configure-button">
<button onClick={onSettingsClick} id="configure-env">
<button onClick={onSettingsClick} id="configure-env" data-testid="configure-env">
<IconSettings size={16} strokeWidth={1.5} />
<span>Configure</span>
</button>

View File

@@ -736,6 +736,7 @@ const EnvironmentList = ({
<CollapsibleSection
title=".env Files"
testId="dotenv-files-section"
expanded={dotEnvExpanded}
onToggle={() => setDotEnvExpanded(!dotEnvExpanded)}
badge={dotEnvFiles.length}
@@ -744,6 +745,7 @@ const EnvironmentList = ({
className="btn-action"
onClick={handleCreateDotEnvInlineClick}
title="Create .env file"
data-testid="create-dotenv-file"
>
<IconPlus size={14} strokeWidth={1.5} />
</button>
@@ -768,6 +770,7 @@ const EnvironmentList = ({
ref={dotEnvInputRef}
type="text"
className="environment-name-input"
data-testid="dotenv-name-input"
value={newDotEnvName}
onChange={handleDotEnvNameChange}
onKeyDown={handleDotEnvNameKeyDown}

View File

@@ -731,6 +731,7 @@ const EnvironmentList = ({
<CollapsibleSection
title=".env Files"
testId="dotenv-files-section"
expanded={dotEnvExpanded}
onToggle={() => setDotEnvExpanded(!dotEnvExpanded)}
badge={dotEnvFiles.length}
@@ -739,6 +740,7 @@ const EnvironmentList = ({
className="btn-action"
onClick={handleCreateDotEnvInlineClick}
title="Create .env file"
data-testid="create-dotenv-file"
>
<IconPlus size={14} strokeWidth={1.5} />
</button>
@@ -763,6 +765,7 @@ const EnvironmentList = ({
ref={dotEnvInputRef}
type="text"
className="environment-name-input"
data-testid="dotenv-name-input"
value={newDotEnvName}
onChange={handleDotEnvNameChange}
onKeyDown={handleDotEnvNameKeyDown}

View File

@@ -1001,6 +1001,11 @@ export const mountScratchCollection = (workspaceUid) => {
brunoConfig
});
// Map scratch collection to workspace so getProcessEnvVars can resolve workspace .env values
if (workspace.pathname) {
await ipcRenderer.invoke('renderer:set-collection-workspace', scratchCollectionUid, workspace.pathname);
}
await dispatch(openScratchCollectionEvent(scratchCollectionUid, tempDirectoryPath, brunoConfig));
dispatch(setWorkspaceScratchCollection({

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "process-env-global-test",
"type": "collection"
}

View File

@@ -0,0 +1,17 @@
meta {
name: echo-post
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: json
auth: none
}
body:json {
{
"value": "{{testVar}}"
}
}

View File

@@ -0,0 +1,63 @@
import { test, expect } from '../../../playwright';
import {
openCollection,
openEnvironmentSelector,
openRequest,
sendRequest,
expectResponseContains
} from '../../utils/page';
test.describe('Global Environment process.env Resolution', () => {
test('should resolve process.env variables referenced in global environment', async ({
pageWithUserData: page
}) => {
await test.step('Open collection', async () => {
await openCollection(page, 'process-env-global-test');
});
await test.step('Create .env file with variable via UI', async () => {
// Open global environment configuration
await openEnvironmentSelector(page, 'global');
await page.getByTestId('configure-env').click();
// Expand the .env Files section
const dotEnvSection = page.getByTestId('dotenv-files-section');
await dotEnvSection.waitFor({ state: 'visible' });
await dotEnvSection.click();
// Click + to create a new .env file
await page.getByTestId('create-dotenv-file').click();
// Accept the default name (.env) and press Enter
await page.getByTestId('dotenv-name-input').press('Enter');
await expect(page.getByText('.env file created!')).toBeVisible();
// Switch to Raw mode to type the variable
await page.getByTestId('dotenv-view-raw').click();
// Type the variable into the raw editor
const rawEditor = page.getByTestId('dotenv-raw-editor').locator('.CodeMirror');
await rawEditor.click();
await page.keyboard.type('MY_SECRET=hello-from-dotenv');
// Save the .env file
await page.getByTestId('save-dotenv-raw').click();
});
await test.step('Verify global environment is active', async () => {
await expect(page.locator('.current-environment')).toContainText('ProcessEnv Test');
});
await test.step('Open request', async () => {
await openRequest(page, 'process-env-global-test', 'echo-post');
});
await test.step('Send request and verify process.env resolved', async () => {
await sendRequest(page, 200);
});
await test.step('Verify response contains resolved value from .env file', async () => {
await expectResponseContains(page, ['hello-from-dotenv']);
});
});
});

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{collectionPath}}",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -0,0 +1,19 @@
{
"environments": [
{
"uid": "pEnvTestGlobalEnvUi01",
"name": "ProcessEnv Test",
"variables": [
{
"uid": "pEnvTestVarUid0000001",
"name": "testVar",
"value": "{{process.env.MY_SECRET}}",
"type": "text",
"secret": false,
"enabled": true
}
]
}
],
"activeGlobalEnvironmentUid": "pEnvTestGlobalEnvUi01"
}

View File

@@ -0,0 +1,12 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{collectionPath}}"
],
"preferences": {
"onboarding": {
"hasLaunchedBefore": true,
"hasSeenWelcomeModal": true
}
}
}