Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
99ed018
feat: Support Multiline Environment Variables
Pragadesh-45 Aug 28, 2025
494a4d6
feat: Refactor multiline string handling in envToJson and jsonToEnv
Pragadesh-45 Sep 2, 2025
38c86b0
refactor: Improve indentation handling in jsonToEnv
Pragadesh-45 Sep 2, 2025
3e8d2bc
refactor: add getValueString to utils
Pragadesh-45 Sep 2, 2025
3785992
refactor: fix indentation for closing triple quotes in getValueString
Pragadesh-45 Sep 2, 2025
91e08e9
refactor: update getValueString
Pragadesh-45 Sep 2, 2025
e696f68
test: create testcases for bruMultiline
Pragadesh-45 Sep 2, 2025
7b8017d
refactor: enhance multiline text block handling in envToJson
Pragadesh-45 Sep 2, 2025
d2b2d40
refactor: fix multiline text block content extraction grammar
Pragadesh-45 Sep 2, 2025
3909ba3
refactor: add indentLevel in jsonToEnv
Pragadesh-45 Sep 2, 2025
a11a9e0
feat: add `normalizeNewlines` utility
Pragadesh-45 Sep 2, 2025
9854c31
docs: add comments for multiline content handling
Pragadesh-45 Sep 2, 2025
6de261f
test: add e2e tests for multiline environment variables
Pragadesh-45 Sep 3, 2025
8dc33ee
chore: remove obsolete bruMultiline tests
Pragadesh-45 Sep 3, 2025
65cdef4
tests: refactor playwright testcases
Pragadesh-45 Sep 3, 2025
011832f
test: update test descriptions for clarity
helloanoop Sep 4, 2025
728f84f
test: refactor multiline environment variable tests
helloanoop Sep 4, 2025
eadf75f
refactor: improve indentation handling in jsonToEnv and utils
Pragadesh-45 Sep 4, 2025
a616d81
test: enhance multiline variable tests and improve environment selector
Pragadesh-45 Sep 5, 2025
b468469
test: add unit test cases for `getValueString`
Pragadesh-45 Sep 5, 2025
5babc11
Remove trailing whitespace trimming in utils.js
helloanoop Sep 6, 2025
b2ff442
Remove test for trimming whitespace
helloanoop Sep 6, 2025
1521870
Correct indentation in multiline value wrapping
helloanoop Sep 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": "1",
"name": "multiline-variables",
"type": "collection"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
meta {
name: multiline-variables
type: collection
version: 1.0.0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
vars {
host: https://www.httpfaker.org
multiline_data: '''
line1
line2
line3
'''
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
meta {
name: multiline-test
type: http
seq: 2
}

post {
url: {{host}}/api/echo
body: json
auth: none
}

body:json {
{{multiline_data_json}}
}

tests {
test("should post multiline data successfully", function() {
expect(res.getStatus()).to.equal(200);
});

test("should resolve multiline_data_json variable correctly", function() {
const body = res.getBody();
// HTTP Faker echo endpoint returns the request body in body.body
// Verify the multiline JSON variable was resolved and parsed correctly
expect(body.body.user.name).to.equal("John Doe");
expect(body.body.user.email).to.equal("[email protected]");
expect(body.body.user.preferences.theme).to.equal("dark");
expect(body.body.user.preferences.notifications).to.equal(true);
});

test("should preserve JSON structure from multiline variable", function() {
const body = res.getBody();
// Verify the complete JSON structure was preserved
expect(body.body.metadata.created).to.equal("2025-09-03");
expect(body.body.metadata.version).to.equal("1.0");
});

test("should resolve host variable in URL", function() {
const body = res.getBody();
// Verify the host variable was resolved in the request URL
expect(body.url).to.equal("https://www.httpfaker.org/api/echo");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
meta {
name: request
type: http
seq: 1
}

post {
url: {{host}}/api/echo
body: text
auth: none
}

body:json {
Ping Test Request
Host: {{host}}

Multiline Data:
{{multiline_data}}

End of multiline content.
}

body:text {
{{host}}
{{multiline_data}}
}

tests {
test("should get 200 response", function() {
expect(res.getStatus()).to.equal(200);
});

test("should resolve multiline_data variable correctly", function() {
const body = res.getBody();
// Verify the multiline variable was resolved and contains all three lines
expect(body.body).to.equal("https://www.httpfaker.org\nline1\nline2\nline3");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/e2e-tests/environments/multiline-variables/collection",
"securityConfig": {
"jsSandboxMode": "developer"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/e2e-tests/environments/multiline-variables/collection"
],
"request": {
"sslVerification": false,
"customCaCertificate": {
"enabled": false,
"filePath": null
}
},
"font": {
"codeFont": "default"
},
"proxy": {
"enabled": false,
"protocol": "http",
"hostname": "",
"port": "",
"auth": {
"enabled": false,
"username": "",
"password": ""
},
"bypassProxy": ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from '../../../playwright';

test.describe('Multiline Variables - Read Environment Test', () => {
test('should read existing multiline environment variables', async ({ pageWithUserData: page }) => {
test.setTimeout(30 * 1000);

// open the collection
await expect(page.getByTitle('multiline-variables')).toBeVisible();
await page.getByTitle('multiline-variables').click();

// open request
await expect(page.getByTitle('request', { exact: true })).toBeVisible();
await page.getByTitle('request', { exact: true }).click();

// open environment dropdown
await expect(page.getByTitle('No Environment')).toBeVisible();
await page.getByTitle('No Environment').click();

// select test environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();
await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();
await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();

// send request
const sendButton = page.locator('#send-request').getByRole('img').nth(2);
await expect(sendButton).toBeVisible();
await sendButton.click();
await expect(page.locator('.response-status-code.text-ok')).toBeVisible();
await expect(page.locator('.response-status-code')).toContainText('200');

// response pane should contain the expected multiline text in JSON body
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('"body": "https://www.httpfaker.org\\nline1\\nline2\\nline3"');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { test, expect } from '../../../playwright';

test.describe('Multiline Variables - Write Test', () => {
test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => {
test.setTimeout(60 * 1000);

// open the collection
await expect(page.getByTitle('multiline-variables')).toBeVisible();
await page.getByTitle('multiline-variables').click();

// open request
await expect(page.getByTitle('multiline-test', { exact: true })).toBeVisible();
await page.getByTitle('multiline-test', { exact: true }).click();

// open environment dropdown
await expect(page.getByTitle('No Environment')).toBeVisible();

Check failure on line 16 in e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[Bruno Electron App] › e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts:4:7 › Multiline Variables - Write Test › should create and use multiline environment variable dynamically

1) [Bruno Electron App] › e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts:4:7 › Multiline Variables - Write Test › should create and use multiline environment variable dynamically Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: getByTitle('No Environment') Expected: visible Received: <element(s) not found> Call log: - expect.toBeVisible with timeout 5000ms - waiting for getByTitle('No Environment') 14 | 15 | // open environment dropdown > 16 | await expect(page.getByTitle('No Environment')).toBeVisible(); | ^ 17 | await page.getByTitle('No Environment').click(); 18 | 19 | // select test environment at /home/runner/work/bruno/bruno/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts:16:53
await page.getByTitle('No Environment').click();

// select test environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();
await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();
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();

// open environment configuration
await expect(page.locator('#Configure')).toBeVisible();
await page.locator('#Configure').click();

// add variable
await page.getByRole('button', { name: /Add.*Variable/i }).click();
const valueTextarea = page.locator('.bruno-modal-card textarea').last();
await expect(valueTextarea).toBeVisible();


const jsonValue = `{
"user": {
"name": "John Doe",
"email": "[email protected]",
"preferences": {
"theme": "dark",
"notifications": true
}
},
"metadata": {
"created": "2025-09-03",
"version": "1.0"
}
}`;

// fill variable value
await valueTextarea.fill(jsonValue);
await page.keyboard.press('Shift+Tab');
await page.keyboard.type('multiline_data_json');

// save variable and close config
const saveVarButton = page.getByRole('button', { name: /Save/i });
await expect(saveVarButton).toBeVisible();
await saveVarButton.click();

await expect(page.locator('.close.cursor-pointer')).toBeVisible();
await page.locator('.close.cursor-pointer').click();

// send request
const sendButton = page.locator('#send-request').getByRole('img').nth(2);
await expect(sendButton).toBeVisible();
await sendButton.click();

// wait for response status
await expect(page.locator('.response-status-code.text-ok')).toBeVisible();
await expect(page.locator('.response-status-code')).toContainText('200');

// verify multiline JSON variable resolution in response
const expectedBody =
'{\n "user": {\n "name": "John Doe",\n "email": "[email protected]",\n "preferences": {\n "theme": "dark",\n "notifications": true\n }\n },\n "metadata": {\n "created": "2025-09-03",\n "version": "1.0"\n }\n}';
await expect(page.locator('.response-pane')).toContainText(`"body": ${JSON.stringify(expectedBody)}`);
});

// clean up created variable after test
test.afterEach(async () => {
const fs = require('fs');
const path = require('path');

const testBruPath = path.join(__dirname, 'collection/environments/Test.bru');
let content = fs.readFileSync(testBruPath, 'utf8');

// remove the multiline_data_json variable and its content
content = content.replace(/\s*multiline_data_json:\s*'''\s*[\s\S]*?\s*'''/g, '');

fs.writeFileSync(testBruPath, content);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<p className="text-nowrap truncate max-w-32" title={activeEnvironment ? activeEnvironment.name : 'No Environment'}>{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);
Expand Down Expand Up @@ -82,7 +82,7 @@ const EnvironmentSelector = ({ collection }) => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<div className="pr-2 text-gray-600" id="Configure">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Configure</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCh
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import MultiLineEditor from 'components/MultiLineEditor';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
import { useFormik } from 'formik';
Expand Down Expand Up @@ -214,7 +214,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</td>
<td className="flex flex-row flex-nowrap items-center">
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
<MultiLineEditor
theme={storedTheme}
collection={_collection}
name={`${index}.value`}
Expand Down Expand Up @@ -253,6 +253,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
ref={addButtonRef}
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
onClick={addVariable}
id="add-variable"
>
+ Add Variable
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import MultiLineEditor from 'components/MultiLineEditor';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
import { useFormik } from 'formik';
Expand Down Expand Up @@ -147,7 +147,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</td>
<td className="flex flex-row flex-nowrap">
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
<MultiLineEditor
theme={storedTheme}
name={`${index}.value`}
value={variable.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const StatusCode = ({ status }) => {
};

return (
<StyledWrapper className={getTabClassname(status)}>
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`}>
{status} {statusCodePhraseMap[status]}
</StyledWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ const Collection = ({ collection, searchText }) => {
onClick={handleCollectionCollapse}
onDoubleClick={handleCollectionDoubleClick}
/>
<div className="ml-1 w-full" id="sidebar-collection-name">
<div className="ml-1 w-full" id="sidebar-collection-name" title={collection.name}>
{collection.name}
</div>
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
Expand Down
Loading
Loading