From db35e7059c0573d7877a43485e2408df6cb0a5e6 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Sat, 6 Sep 2025 17:17:51 +0530 Subject: [PATCH] Merge pull request #5438 from Pragadesh-45/feat/multiline-values-for-env-vars Feat/ Add Multiline Support for Enviroment Variables --- .../multiline-variables/collection/bruno.json | 5 + .../collection/collection.bru | 5 + .../collection/environments/Test.bru | 8 ++ .../collection/multiline-test.bru | 44 +++++++ .../collection/request.bru | 38 ++++++ .../init-user-data/collection-security.json | 10 ++ .../init-user-data/preferences.json | 28 +++++ .../read-multiline-environment.spec.ts | 35 ++++++ .../write-multiline-variable.spec.ts | 94 ++++++++++++++ .../Environments/EnvironmentSelector/index.js | 4 +- .../EnvironmentVariables/index.js | 5 +- .../EnvironmentVariables/index.js | 4 +- .../ResponsePane/StatusCode/index.js | 2 +- .../Sidebar/Collections/Collection/index.js | 2 +- packages/bruno-lang/v2/src/envToJson.js | 43 ++++++- packages/bruno-lang/v2/src/jsonToBru.js | 19 +-- packages/bruno-lang/v2/src/jsonToEnv.js | 6 +- packages/bruno-lang/v2/src/utils.js | 35 +++++- .../bruno-lang/v2/tests/envToJson.spec.js | 112 +++++++++++++++++ .../bruno-lang/v2/tests/jsonToEnv.spec.js | 117 ++++++++++++++++-- packages/bruno-lang/v2/tests/utils.spec.js | 21 ++++ 21 files changed, 596 insertions(+), 41 deletions(-) create mode 100644 e2e-tests/environments/multiline-variables/collection/bruno.json create mode 100644 e2e-tests/environments/multiline-variables/collection/collection.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/environments/Test.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/multiline-test.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/request.bru create mode 100644 e2e-tests/environments/multiline-variables/init-user-data/collection-security.json create mode 100644 e2e-tests/environments/multiline-variables/init-user-data/preferences.json create mode 100644 e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts create mode 100644 e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts create mode 100644 packages/bruno-lang/v2/tests/utils.spec.js diff --git a/e2e-tests/environments/multiline-variables/collection/bruno.json b/e2e-tests/environments/multiline-variables/collection/bruno.json new file mode 100644 index 000000000..b2f6c48cd --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "multiline-variables", + "type": "collection" +} diff --git a/e2e-tests/environments/multiline-variables/collection/collection.bru b/e2e-tests/environments/multiline-variables/collection/collection.bru new file mode 100644 index 000000000..66d9f88e4 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/collection.bru @@ -0,0 +1,5 @@ +meta { + name: multiline-variables + type: collection + version: 1.0.0 +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/collection/environments/Test.bru b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru new file mode 100644 index 000000000..7a9190cd1 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru @@ -0,0 +1,8 @@ +vars { + host: https://www.httpfaker.org + multiline_data: ''' + line1 + line2 + line3 + ''' +} diff --git a/e2e-tests/environments/multiline-variables/collection/multiline-test.bru b/e2e-tests/environments/multiline-variables/collection/multiline-test.bru new file mode 100644 index 000000000..4f19735c9 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/multiline-test.bru @@ -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("john@example.com"); + 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"); + }); +} diff --git a/e2e-tests/environments/multiline-variables/collection/request.bru b/e2e-tests/environments/multiline-variables/collection/request.bru new file mode 100644 index 000000000..1f82b0950 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/request.bru @@ -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"); + }); +} diff --git a/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json b/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json new file mode 100644 index 000000000..5f51bd286 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/e2e-tests/environments/multiline-variables/collection", + "securityConfig": { + "jsSandboxMode": "developer" + } + } + ] +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/init-user-data/preferences.json b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json new file mode 100644 index 000000000..9374c16fd --- /dev/null +++ b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json @@ -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": "" + } +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts new file mode 100644 index 000000000..0e0856816 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts @@ -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"'); + }); +}); diff --git a/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts b/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts new file mode 100644 index 000000000..f2aff96fe --- /dev/null +++ b/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts @@ -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(); + 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": "john@example.com", + "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": "john@example.com",\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); + }); +}); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index 848048c13..a8246237c 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => { const Icon = forwardRef((props, ref) => { return (
-

{activeEnvironment ? activeEnvironment.name : 'No Environment'}

+

{activeEnvironment ? activeEnvironment.name : 'No Environment'}

); @@ -82,7 +82,7 @@ const EnvironmentSelector = ({ collection }) => { handleSettingsIconClick(); dropdownTippyRef.current.hide(); }}> -
+
Configure diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 5ba3b0797..d02cc494a 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -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'; @@ -214,7 +214,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
- + Add Variable diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 10ab9fba3..f539543dd 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -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'; @@ -147,7 +147,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
- { }; return ( - + {status} {statusCodePhraseMap[status]} ); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 0f44b467a..67ffcb68b 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -232,7 +232,7 @@ const Collection = ({ collection, searchText }) => { onClick={handleCollectionCollapse} onDoubleClick={handleCollectionDoubleClick} /> -