From 99ed0181140a29908ef20bb69d514c6e4e60fd36 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Thu, 28 Aug 2025 10:55:44 +0545 Subject: [PATCH 01/23] feat: Support Multiline Environment Variables - Updated SingleLineEditor to allow newlines when `allowNewlines` prop is true, otherwise runs the handler on Shift-Enter. - Modified grammar in envToJson to support multiline text blocks with triple quotes. - Updated jsonToEnv to format values with newlines using triple quotes for better readability. - refactor: replace `SingleLineEditor` with `MultilineEditor` to support newLines --- .../EnvironmentVariables/index.js | 4 +- .../EnvironmentVariables/index.js | 4 +- packages/bruno-lang/v2/src/envToJson.js | 35 ++++- packages/bruno-lang/v2/src/jsonToEnv.js | 8 ++ .../bruno-lang/v2/tests/envToJson.spec.js | 112 ++++++++++++++++ .../bruno-lang/v2/tests/jsonToEnv.spec.js | 122 ++++++++++++++++++ 6 files changed, 279 insertions(+), 6 deletions(-) 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 5ba3b07971..ab0220b276 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
-
- { + // Remove leading spaces for first and last lines, keep indentation for content lines + if (index === 0 || index === multilineString.split('\n').length - 1) { + return line.trim(); + } + // Remove standard 4-space indentation + return line.startsWith(' ') ? line.slice(4) : line; + }) + // Remove empty first/last lines + .filter(line => line !== '') + .join('\n'); + } + return chars.sourceString ? chars.sourceString.trim() : ''; + } catch (err) { + console.error(err); + } return chars.sourceString ? chars.sourceString.trim() : ''; }, + multilinetextblockdelimiter(_) { + return ''; + }, + multilinetextblock(_1, content, _2) { + return content.sourceString.trim(); + }, nl(_1, _2) { return ''; }, diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index 42d0a4281d..debcd14c0c 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -7,6 +7,14 @@ const envToJson = (json) => { .map((variable) => { const { name, value, enabled } = variable; const prefix = enabled ? '' : '~'; + + // Check if value contains newlines or is formatted JSON/object + if (value && (value.includes('\n') || value.includes('\r'))) { + // Use multiline format with triple quotes + const indentedValue = value.split('\n').map(line => ` ${line}`).join('\n'); + return ` ${prefix}${name}: '''\n${indentedValue}\n '''`; + } + return ` ${prefix}${name}: ${value}`; }); diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js index 9c97cd1fa5..a6352a6144 100644 --- a/packages/bruno-lang/v2/tests/envToJson.spec.js +++ b/packages/bruno-lang/v2/tests/envToJson.spec.js @@ -313,4 +313,116 @@ vars:secret [access_key,access_secret, access_password ] expect(output).toEqual(expected); }); + + it('should parse multiline variable values', () => { + const input = ` +vars { + json_data: ''' + { + "name": "test", + "value": 123 + } + ''' +}`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'json_data', + value: '{\n "name": "test",\n "value": 123\n}', + enabled: true, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse multiline variable with mixed indentation', () => { + const input = ` +vars { + script: ''' + function test() { + console.log("hello"); + return true; + } + ''' +}`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'script', + value: 'function test() {\n console.log("hello");\n return true;\n}', + enabled: true, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse disabled multiline variable', () => { + const input = ` +vars { + ~disabled_multiline: ''' + line 1 + line 2 + line 3 + ''' +}`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'disabled_multiline', + value: 'line 1\nline 2\nline 3', + enabled: false, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse multiple multiline variables', () => { + const input = ` +vars { + config: ''' + debug=true + port=3000 + ''' + template: ''' + + Hello World + + ''' +}`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'config', + value: 'debug=true\nport=3000', + enabled: true, + secret: false + }, + { + name: 'template', + value: '\n Hello World\n', + enabled: true, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); }); diff --git a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js index 62b7aa2697..90193f41c8 100644 --- a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js +++ b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js @@ -137,6 +137,128 @@ vars:secret [ const expected = `vars:secret [ token ] +`; + expect(output).toEqual(expected); + }); + + it('should generate multiline variable values', () => { + const input = { + variables: [ + { + name: 'json_data', + value: '{\n "name": "test",\n "value": 123\n}', + enabled: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + json_data: ''' + { + "name": "test", + "value": 123 + } + ''' +} +`; + expect(output).toEqual(expected); + }); + + it('should generate multiline variable with proper indentation', () => { + const input = { + variables: [ + { + name: 'script', + value: 'function test() {\n console.log("hello");\n return true;\n}', + enabled: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + script: ''' + function test() { + console.log("hello"); + return true; + } + ''' +} +`; + expect(output).toEqual(expected); + }); + + it('should generate disabled multiline variable', () => { + const input = { + variables: [ + { + name: 'disabled_multiline', + value: 'line 1\nline 2\nline 3', + enabled: false + } + ] + }; + + const output = parser(input); + const expected = `vars { + ~disabled_multiline: ''' + line 1 + line 2 + line 3 + ''' +} +`; + expect(output).toEqual(expected); + }); + + it('should generate multiple multiline variables', () => { + const input = { + variables: [ + { + name: 'config', + value: 'debug=true\nport=3000', + enabled: true + }, + { + name: 'template', + value: '\n Hello World\n', + enabled: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + config: ''' + debug=true + port=3000 + ''' + template: ''' + + Hello World + + ''' +} +`; + expect(output).toEqual(expected); + }); + + it('should handle single line values normally', () => { + const input = { + variables: [ + { + name: 'simple', + value: 'single line value', + enabled: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + simple: single line value +} `; expect(output).toEqual(expected); }); From 494a4d6e175cd80a7f31c3762d73b8024532f9a7 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 18:44:51 +0545 Subject: [PATCH 02/23] feat: Refactor multiline string handling in envToJson and jsonToEnv - Simplified multiline string processing in envToJson by removing unnecessary checks and directly slicing leading spaces. - Introduced getValueString function in jsonToEnv to handle multiline strings with proper indentation and formatting. - Added indentString4 utility function for consistent 4-space indentation across the codebase. --- packages/bruno-lang/v2/src/envToJson.js | 11 +---------- packages/bruno-lang/v2/src/jsonToEnv.js | 26 +++++++++++++++++-------- packages/bruno-lang/v2/src/utils.js | 14 ++++++++++++- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index 55ed4a9b69..680963f761 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -129,16 +129,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { const multilineString = chars.sourceString?.replace(/^'''|'''$/g, ''); return multilineString .split('\n') - .map((line, index) => { - // Remove leading spaces for first and last lines, keep indentation for content lines - if (index === 0 || index === multilineString.split('\n').length - 1) { - return line.trim(); - } - // Remove standard 4-space indentation - return line.startsWith(' ') ? line.slice(4) : line; - }) - // Remove empty first/last lines - .filter(line => line !== '') + .map((line) => line.slice(4)) .join('\n'); } return chars.sourceString ? chars.sourceString.trim() : ''; diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index debcd14c0c..aa70b1024c 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -1,5 +1,22 @@ const _ = require('lodash'); +const getValueString = (value) => { + const hasNewLines = value?.includes('\n'); + + if (!hasNewLines) { + return value; + } + + // Add 4-space indentation to the contents of the multiline string + const indentedLines = value + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + + // Join the lines back together with newline characters and enclose them in triple single quotes + return `'''\n${indentedLines}\n'''`; +}; + const envToJson = (json) => { const variables = _.get(json, 'variables', []); const vars = variables @@ -8,14 +25,7 @@ const envToJson = (json) => { const { name, value, enabled } = variable; const prefix = enabled ? '' : '~'; - // Check if value contains newlines or is formatted JSON/object - if (value && (value.includes('\n') || value.includes('\r'))) { - // Use multiline format with triple quotes - const indentedValue = value.split('\n').map(line => ` ${line}`).join('\n'); - return ` ${prefix}${name}: '''\n${indentedValue}\n '''`; - } - - return ` ${prefix}${name}: ${value}`; + return ` ${prefix}${name}: ${getValueString(value)}`; }); const secretVars = variables diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 454d2099d6..7e75698a59 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -18,11 +18,22 @@ const indentString = (str) => { .join('\n'); }; +const indentString4 = (str) => { + if (!str || !str.length) { + return str || ''; + } + + return str + .split('\n') + .map((line) => ' ' + line) + .join('\n'); +}; + const outdentString = (str) => { if (!str || !str.length) { return str || ''; } - + return str .split('\n') .map((line) => line.replace(/^ /, '')) @@ -32,5 +43,6 @@ const outdentString = (str) => { module.exports = { safeParseJson, indentString, + indentString4, outdentString }; From 38c86b0198e80dca915b0463bdb3716dfd375caa Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 18:46:58 +0545 Subject: [PATCH 03/23] refactor: Improve indentation handling in jsonToEnv --- packages/bruno-lang/v2/src/jsonToEnv.js | 9 ++------- packages/bruno-lang/v2/src/utils.js | 17 +++-------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index aa70b1024c..b222d4b359 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -1,4 +1,5 @@ const _ = require('lodash'); +const { indentString } = require('./utils'); const getValueString = (value) => { const hasNewLines = value?.includes('\n'); @@ -7,14 +8,8 @@ const getValueString = (value) => { return value; } - // Add 4-space indentation to the contents of the multiline string - const indentedLines = value - .split('\n') - .map((line) => ` ${line}`) - .join('\n'); - // Join the lines back together with newline characters and enclose them in triple single quotes - return `'''\n${indentedLines}\n'''`; + return `'''\n${indentString(value, 4)}\n'''`; }; const envToJson = (json) => { diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 7e75698a59..75bf4fb1a9 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -7,25 +7,15 @@ const safeParseJson = (json) => { } }; -const indentString = (str) => { +const indentString = (str, indentLevel = 2) => { if (!str || !str.length) { return str || ''; } + const indent = ' '.repeat(indentLevel); return str .split('\n') - .map((line) => ' ' + line) - .join('\n'); -}; - -const indentString4 = (str) => { - if (!str || !str.length) { - return str || ''; - } - - return str - .split('\n') - .map((line) => ' ' + line) + .map((line) => indent + line) .join('\n'); }; @@ -43,6 +33,5 @@ const outdentString = (str) => { module.exports = { safeParseJson, indentString, - indentString4, outdentString }; From 3e8d2bcff94cc714eafac03673c77b67d00e2c5f Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 18:55:00 +0545 Subject: [PATCH 04/23] refactor: add getValueString to utils --- packages/bruno-lang/v2/src/jsonToBru.js | 19 +------------------ packages/bruno-lang/v2/src/jsonToEnv.js | 15 ++------------- packages/bruno-lang/v2/src/utils.js | 14 +++++++++++++- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 26de19b510..f8e3652df8 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -1,6 +1,6 @@ const _ = require('lodash'); -const { indentString } = require('./utils'); +const { indentString, getValueString } = require('./utils'); const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]); const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]); @@ -12,23 +12,6 @@ const stripLastLine = (text) => { return text.replace(/(\r?\n)$/, ''); }; -const getValueString = (value) => { - const hasNewLines = value?.includes('\n'); - - if (!hasNewLines) { - return value; - } - - // Add one level of indentation to the contents of the multistring - const indentedLines = value - .split('\n') - .map((line) => ` ${line}`) - .join('\n'); - - // Join the lines back together with newline characters and enclose them in triple single quotes - return `'''\n${indentedLines}\n'''`; -}; - const jsonToBru = (json) => { const { meta, http, grpc, params, headers, metadata, auth, body, script, tests, vars, assertions, settings, docs } = json; diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index b222d4b359..0cd9a9d776 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -1,16 +1,5 @@ const _ = require('lodash'); -const { indentString } = require('./utils'); - -const getValueString = (value) => { - const hasNewLines = value?.includes('\n'); - - if (!hasNewLines) { - return value; - } - - // Join the lines back together with newline characters and enclose them in triple single quotes - return `'''\n${indentString(value, 4)}\n'''`; -}; +const { getValueString } = require('./utils'); const envToJson = (json) => { const variables = _.get(json, 'variables', []); @@ -20,7 +9,7 @@ const envToJson = (json) => { const { name, value, enabled } = variable; const prefix = enabled ? '' : '~'; - return ` ${prefix}${name}: ${getValueString(value)}`; + return ` ${prefix}${name}: ${getValueString(value, 4)}`; }); const secretVars = variables diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 75bf4fb1a9..b077cc6564 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -30,8 +30,20 @@ const outdentString = (str) => { .join('\n'); }; +const getValueString = (value, indentLevel = 2) => { + const hasNewLines = value?.includes('\n'); + + if (!hasNewLines) { + return value; + } + + // Join the lines back together with newline characters and enclose them in triple single quotes + return `'''\n${indentString(value, indentLevel)}\n'''`; +}; + module.exports = { safeParseJson, indentString, - outdentString + outdentString, + getValueString }; From 3785992140fb5a36b39135a172611eb7b3756c89 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 18:56:33 +0545 Subject: [PATCH 05/23] refactor: fix indentation for closing triple quotes in getValueString --- packages/bruno-lang/v2/src/utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index b077cc6564..03eb1675bf 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -38,7 +38,9 @@ const getValueString = (value, indentLevel = 2) => { } // Join the lines back together with newline characters and enclose them in triple single quotes - return `'''\n${indentString(value, indentLevel)}\n'''`; + // The closing ''' should be indented at the same level as the key (2 spaces for env files) + const closingIndent = ' '.repeat(2); + return `'''\n${indentString(value, indentLevel)}\n${closingIndent}'''`; }; module.exports = { From 91e08e90cb9051bc928ca7273ec34e1bd011982e Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 19:02:42 +0545 Subject: [PATCH 06/23] refactor: update getValueString --- packages/bruno-lang/v2/src/utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 03eb1675bf..86c772ff94 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -38,8 +38,9 @@ const getValueString = (value, indentLevel = 2) => { } // Join the lines back together with newline characters and enclose them in triple single quotes - // The closing ''' should be indented at the same level as the key (2 spaces for env files) - const closingIndent = ' '.repeat(2); + // For env files, the closing ''' needs 2-space indent to align with the key + // For bru files, this gets wrapped with indentString() so no closing indent needed here + const closingIndent = indentLevel > 2 ? ' ' : ''; return `'''\n${indentString(value, indentLevel)}\n${closingIndent}'''`; }; From e696f68757fbe11163c2573a81b72e9d11c77fcf Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:12:08 +0545 Subject: [PATCH 07/23] test: create testcases for bruMultiline --- .../bruno-lang/v2/tests/bruMultiline.spec.js | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 packages/bruno-lang/v2/tests/bruMultiline.spec.js diff --git a/packages/bruno-lang/v2/tests/bruMultiline.spec.js b/packages/bruno-lang/v2/tests/bruMultiline.spec.js new file mode 100644 index 0000000000..023e0f254f --- /dev/null +++ b/packages/bruno-lang/v2/tests/bruMultiline.spec.js @@ -0,0 +1,270 @@ +const parser = require('../src/bruToJson.js'); + +describe('bru parser multiline', () => { + it('should parse multiline body:json with proper indentation', () => { + const input = ` +meta { + name: test +} + +post { + url: https://example.com +} + +body:json { + ''' + { + "name": "test", + "value": 123 + } + ''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'post', + url: 'https://example.com' + }, + body: { + json: "\'\'\'\n{\n \"name\": \"test\",\n \"value\": 123\n}\n\'\'\'" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse multiline body:text with proper indentation', () => { + const input = ` +meta { + name: test +} + +post { + url: https://example.com +} + +body:text { + ''' + Hello World + This is a multiline + text content + ''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'post', + url: 'https://example.com' + }, + body: { + text: "\'\'\'\nHello World\nThis is a multiline\ntext content\n\'\'\'" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse multiline script:pre-request with proper indentation', () => { + const input = ` +meta { + name: test +} + +get { + url: https://example.com +} + +script:pre-request { + ''' + console.log('Starting request'); + bru.setVar('timestamp', Date.now()); + ''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'get', + url: 'https://example.com' + }, + script: { + req: "\'\'\'\nconsole.log('Starting request');\nbru.setVar('timestamp', Date.now());\n\'\'\'" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse multiline tests with proper indentation', () => { + const input = ` +meta { + name: test +} + +get { + url: https://example.com +} + +tests { + ''' + test('Status code is 200', function() { + expect(res.getStatus()).to.equal(200); + }); + + test('Response has data', function() { + expect(res.getBody()).to.have.property('data'); + }); + ''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'get', + url: 'https://example.com' + }, + tests: "\'\'\'\ntest('Status code is 200', function() {\n expect(res.getStatus()).to.equal(200);\n});\n\ntest('Response has data', function() {\n expect(res.getBody()).to.have.property('data');\n});\n\'\'\'" + }; + expect(output).toEqual(expected); + }); + + it('should NOT treat single-line content with triple quotes as multiline', () => { + const input = ` +meta { + name: test +} + +post { + url: https://example.com +} + +body:json { + '''{"name": "test"}''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'post', + url: 'https://example.com' + }, + body: { + json: '\'\'\'{"name": "test"}\'\'\'' + } + }; + expect(output).toEqual(expected); + }); + + it('should handle mixed content in body:json', () => { + const input = ` +meta { + name: test +} + +post { + url: https://example.com +} + +body:json { + { + "name": "test", + "nested": { + "value": 123 + } + } +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'post', + url: 'https://example.com' + }, + body: { + json: '{\n "name": "test",\n "nested": {\n "value": 123\n }\n}' + } + }; + expect(output).toEqual(expected); + }); + + it('should parse multiline dictionary values with proper indentation', () => { + const input = ` +meta { + name: test +} + +post { + url: https://example.com +} + +headers { + content-type: application/json + custom-header: ''' + This is a multiline + header value + ''' +} +`; + + const output = parser(input); + const expected = { + meta: { + name: 'test', + seq: 1, + type: 'http' + }, + http: { + method: 'post', + url: 'https://example.com' + }, + headers: [ + { + name: 'content-type', + value: 'application/json', + enabled: true + }, + { + name: 'custom-header', + value: 'is is a multiline\nader value', + enabled: true + } + ] + }; + expect(output).toEqual(expected); + }); +}); From 7b8017d80529a571b08f4a0c350366f0c712a5bc Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:15:06 +0545 Subject: [PATCH 08/23] refactor: enhance multiline text block handling in envToJson - Updated grammar to define start and end for multiline text blocks. - Simplified value extraction for multiline strings by directly returning the AST. - Improved indentation handling by removing leading spaces from multiline content. --- packages/bruno-lang/v2/src/envToJson.js | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index 680963f761..db2dc3c7e9 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -1,6 +1,7 @@ const ohm = require('ohm-js'); const _ = require('lodash'); +const indentLevel = 4; const grammar = ohm.grammar(`Bru { BruEnvFile = (vars | secretvars)* @@ -10,10 +11,13 @@ const grammar = ohm.grammar(`Bru { tagend = nl "}" optionalnl = ~tagend nl keychar = ~(tagend | st | nl | ":") any - valuechar = ~(nl | tagend | multilinetextblockdelimiter) any + valuechar = ~(nl | tagend | multilinetextblockstart) any multilinetextblockdelimiter = "'''" - multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter + multilinetextblockstart = "'''" nl + multilinetextblockend = nl st* "'''" + multilinetextblock = multilinetextblockstart multilinetextblockcontent multilinetextblockend + multilinetextblockcontent = (~(nl st* "'''") any)* // Dictionary Blocks dictionary = st* "{" pairlist? tagend @@ -123,26 +127,30 @@ const sem = grammar.createSemantics().addAttribute('ast', { return chars.sourceString ? chars.sourceString.trim() : ''; }, value(chars) { - try { - let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`); - if (isMultiline) { - const multilineString = chars.sourceString?.replace(/^'''|'''$/g, ''); - return multilineString - .split('\n') - .map((line) => line.slice(4)) - .join('\n'); - } - return chars.sourceString ? chars.sourceString.trim() : ''; - } catch (err) { - console.error(err); + // .ctorName provides the name of the rule that matched the input + if (chars.ctorName === 'multilinetextblock') { + return chars.ast; } return chars.sourceString ? chars.sourceString.trim() : ''; }, + multilinetextblockstart(_1, _2) { + return ''; + }, + multilinetextblockend(_1, _2, _3) { + return ''; + }, multilinetextblockdelimiter(_) { return ''; }, multilinetextblock(_1, content, _2) { - return content.sourceString.trim(); + return content.ast + .split('\n') + .map((line) => line.slice(indentLevel)) // Remove 4-space indentation + .join('\n') + .trim(); // Remove leading/trailing empty lines + }, + multilinetextblockcontent(chars) { + return chars.sourceString; }, nl(_1, _2) { return ''; From d2b2d40cff3f96a033716cc91ace688a4940a00f Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:41:53 +0545 Subject: [PATCH 09/23] refactor: fix multiline text block content extraction grammar --- packages/bruno-lang/v2/src/envToJson.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index db2dc3c7e9..5d99b0a630 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -17,7 +17,7 @@ const grammar = ohm.grammar(`Bru { multilinetextblockstart = "'''" nl multilinetextblockend = nl st* "'''" multilinetextblock = multilinetextblockstart multilinetextblockcontent multilinetextblockend - multilinetextblockcontent = (~(nl st* "'''") any)* + multilinetextblockcontent = (~multilinetextblockend any)* // Dictionary Blocks dictionary = st* "{" pairlist? tagend From 3909ba333f0c947747f3d5e5c961a0725e7988e4 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:49:51 +0545 Subject: [PATCH 10/23] refactor: add indentLevel in jsonToEnv --- packages/bruno-lang/v2/src/jsonToEnv.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index 0cd9a9d776..f58b150c9c 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -1,6 +1,7 @@ const _ = require('lodash'); const { getValueString } = require('./utils'); +const indentLevel = 4; const envToJson = (json) => { const variables = _.get(json, 'variables', []); const vars = variables @@ -9,7 +10,7 @@ const envToJson = (json) => { const { name, value, enabled } = variable; const prefix = enabled ? '' : '~'; - return ` ${prefix}${name}: ${getValueString(value, 4)}`; + return ` ${prefix}${name}: ${getValueString(value, indentLevel)}`; }); const secretVars = variables From a11a9e0e214531986f35d0434c1201bfccb820e2 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:50:41 +0545 Subject: [PATCH 11/23] feat: add `normalizeNewlines` utility --- packages/bruno-lang/v2/src/utils.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 86c772ff94..2a909874ca 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -7,13 +7,21 @@ const safeParseJson = (json) => { } }; +const normalizeNewlines = (str) => { + if (!str || typeof str !== 'string') { + return str || ''; + } + + return str.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); +}; + const indentString = (str, indentLevel = 2) => { if (!str || !str.length) { return str || ''; } const indent = ' '.repeat(indentLevel); - return str + return normalizeNewlines(str) .split('\n') .map((line) => indent + line) .join('\n'); @@ -24,14 +32,14 @@ const outdentString = (str) => { return str || ''; } - return str + return normalizeNewlines(str) .split('\n') .map((line) => line.replace(/^ /, '')) .join('\n'); }; const getValueString = (value, indentLevel = 2) => { - const hasNewLines = value?.includes('\n'); + const hasNewLines = value?.includes('\n') || value?.includes('\r'); if (!hasNewLines) { return value; @@ -46,6 +54,7 @@ const getValueString = (value, indentLevel = 2) => { module.exports = { safeParseJson, + normalizeNewlines, indentString, outdentString, getValueString From 9854c31c8d034f4bd95794dd73c61ac077674604 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 2 Sep 2025 20:59:22 +0545 Subject: [PATCH 12/23] docs: add comments for multiline content handling --- packages/bruno-lang/v2/src/envToJson.js | 11 ++++++++++- packages/bruno-lang/v2/src/jsonToEnv.js | 9 +++++++++ packages/bruno-lang/v2/src/utils.js | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index 5d99b0a630..b698c8bfc1 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -1,6 +1,15 @@ const ohm = require('ohm-js'); const _ = require('lodash'); +// Env files use 4-space indentation for multiline content +// vars { +// API_KEY: ''' +// -----BEGIN PUBLIC KEY----- +// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8 +// HMR5LXFFrwXQFE6xUVhXrxUpx1TtfoGkRcU7LEWV +// -----END PUBLIC KEY----- +// ''' +// } const indentLevel = 4; const grammar = ohm.grammar(`Bru { BruEnvFile = (vars | secretvars)* @@ -147,7 +156,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { .split('\n') .map((line) => line.slice(indentLevel)) // Remove 4-space indentation .join('\n') - .trim(); // Remove leading/trailing empty lines + .trim(); }, multilinetextblockcontent(chars) { return chars.sourceString; diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index f58b150c9c..471ec988b3 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -1,6 +1,15 @@ const _ = require('lodash'); const { getValueString } = require('./utils'); +// Env files use 4-space indentation for multiline content +// vars { +// API_KEY: ''' +// -----BEGIN PUBLIC KEY----- +// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8 +// HMR5LXFFrwXQFE6xUVhXrxUpx1TtfoGkRcU7LEWV +// -----END PUBLIC KEY----- +// ''' +// } const indentLevel = 4; const envToJson = (json) => { const variables = _.get(json, 'variables', []); diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index 2a909874ca..bb39c6f7a2 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -12,6 +12,7 @@ const normalizeNewlines = (str) => { return str || ''; } + // "\r\n" is windows, "\r" is old mac, "\n" is linux return str.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); }; From 6de261f72bfbf5ae9f406f7744a622677507573d Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 3 Sep 2025 21:09:39 +0545 Subject: [PATCH 13/23] test: add e2e tests for multiline environment variables - Implemented tests to read and create multiline environment variables. - Verified successful request execution and response handling for multiline JSON data. - Added necessary collection and environment files for testing. --- .../001-read-multiline-environment.spec.ts | 65 ++++++++ ...reate-and-parse-multiline-variable.spec.ts | 149 ++++++++++++++++++ .../collection/bruno.json | 5 + .../collection/collection.bru | 5 + .../collection/environments/Test.bru | 8 + .../collection/multiline-test.bru | 21 +++ .../collection/ping-request.bru | 24 +++ .../init-user-data/preferences.json | 28 ++++ 8 files changed, 305 insertions(+) create mode 100644 e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts create mode 100644 e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts create mode 100644 e2e-tests/environment-multiline-variables/collection/bruno.json create mode 100644 e2e-tests/environment-multiline-variables/collection/collection.bru create mode 100644 e2e-tests/environment-multiline-variables/collection/environments/Test.bru create mode 100644 e2e-tests/environment-multiline-variables/collection/multiline-test.bru create mode 100644 e2e-tests/environment-multiline-variables/collection/ping-request.bru create mode 100644 e2e-tests/environment-multiline-variables/init-user-data/preferences.json diff --git a/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts b/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts new file mode 100644 index 0000000000..e3bed2f6ee --- /dev/null +++ b/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '../../playwright'; + +test.describe('Multiline Variables - Read Environment Test', () => { + test('should read existing multiline environment variables and execute request', async ({ pageWithUserData: page }) => { + test.setTimeout(30 * 1000); + + // Step 1: Initialize collection (handle both fresh and configured states) + await page.locator('#sidebar-collection-name').click(); + + // Wait for any dialogs to appear and handle them + await page.waitForTimeout(1500); + + // Handle collection security dialog if it appears + const safeModeLabel = page.getByLabel('Safe Mode'); + const safeModeRadio = page.getByRole('radio', { name: 'Safe Mode' }); + const saveButton = page.getByRole('button', { name: 'Save' }); + + if (await safeModeLabel.isVisible()) { + await safeModeLabel.check(); + await saveButton.click(); + } else if (await safeModeRadio.isVisible()) { + await safeModeRadio.click(); + await saveButton.click(); + } + // If neither is visible, collection is already configured + + // Step 2: Navigate to ping-request + await page.getByText('ping-request', { exact: true }).click(); + + // Step 3: Ensure Test environment is selected + // Handle multiple possible states: "No Environment", "Test", or already configured + const noEnvButton = page.getByText('No Environment'); + const testEnvButton = page.locator('.current-environment').filter({ hasText: /Test/ }); + + if (await noEnvButton.isVisible()) { + // Environment not set, select Test + await noEnvButton.click(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + } else if (await testEnvButton.isVisible()) { + // Test environment already selected, continue + console.log('Test environment already selected'); + } else { + // Try to find and select Test environment from dropdown + try { + await page.locator('.environment-selector').click({ timeout: 2000 }); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + } catch { + console.log('Environment setup complete'); + } + } + + // Step 4: Execute request to verify multiline environment variables work + await page.locator('#send-request').getByRole('img').nth(2).click(); + + // Verify successful response (confirms multiline env vars are parsed correctly) + await expect(page.getByText('200')).toBeVisible({ timeout: 15000 }); + + // Additional verification: check that host variable was resolved in the response + await page.waitForTimeout(2000); + + // Verify the response contains the resolved host variable value + // Use main role to target response area specifically (avoids request panel duplication) + await expect(page.getByRole('main')).toContainText('httpfaker.org', { timeout: 5000 }); + }); +}); diff --git a/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts b/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts new file mode 100644 index 0000000000..9d3a3def94 --- /dev/null +++ b/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts @@ -0,0 +1,149 @@ +import { test, expect } from '../../playwright'; + +test.describe('Multiline Variables - Create and Parse Test', () => { + test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => { + test.setTimeout(60 * 1000); + + // Step 1: Initialize collection (handle both fresh and configured states) + await page.locator('#sidebar-collection-name').click(); + + // Wait for any dialogs to appear and handle them + await page.waitForTimeout(1500); + + // Handle collection security dialog if it appears + const safeModeLabel = page.getByLabel('Safe Mode'); + const safeModeRadio = page.getByRole('radio', { name: 'Safe Mode' }); + const saveButton = page.getByRole('button', { name: 'Save' }); + + if (await safeModeLabel.isVisible()) { + await safeModeLabel.check(); + await saveButton.click(); + } else if (await safeModeRadio.isVisible()) { + await safeModeRadio.click(); + await saveButton.click(); + } + // If neither is visible, collection is already configured + + // Step 2: Handle environment selection and setup (from main collection view) + // Ensure Test environment is selected before configuring variables + const noEnvButton = page.getByText('No Environment'); + const testEnvButton = page.locator('.current-environment').filter({ hasText: /Test/ }); + + if (await noEnvButton.isVisible()) { + // Environment not set, select Test + await noEnvButton.click(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + } else if (await testEnvButton.isVisible()) { + // Test environment already selected, continue + console.log('Test environment already selected'); + } else { + // Try to find and select Test environment from dropdown + try { + await page.locator('.environment-selector').click({ timeout: 2000 }); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + } catch { + console.log('Environment setup complete'); + } + } + + // Step 3: Open environment configuration to create the multiline variable + // Handle different ways the environment dropdown might appear + try { + await page.locator('.current-environment').filter({ hasText: /Test/ }).click(); + await page.getByText('Configure', { exact: true }).click(); + } catch { + // Alternative approach if the above fails + try { + await page.locator('.environment-selector').click(); + await page.getByText('Configure', { exact: true }).click(); + } catch { + // If configure is already visible, just click it + await page.getByText('Configure', { exact: true }).click(); + } + } + + // Step 4: Create the multiline environment variable + await page.getByRole('button', { name: /Add.*Variable/i }).click(); + + // Wait for the form to appear + await page.waitForTimeout(1000); + + // Add multiline JSON value first + const jsonValue = `{ + "user": { + "name": "John Doe", + "email": "john@example.com", + "preferences": { + "theme": "dark", + "notifications": true + } + }, + "metadata": { + "created": "2025-09-03", + "version": "1.0" + } +}`; + + // Fill the value textarea first (we know this works) + const valueInput = page.locator('textarea').last(); + await valueInput.fill(jsonValue); + + // Navigate backwards to the name input using Shift+Tab + await page.keyboard.press('Shift+Tab'); + await page.keyboard.type('multiline_data_json'); + + // Save the environment variable + await page.getByRole('button', { name: /Save/i }).click(); + + // Close environment config + await page.getByText('×').click(); + + // Step 5: Navigate to multiline-test request and verify variable usage + // (The request is already configured to use {{multiline_data_json}}) + await page.getByText('multiline-test', { exact: true }).click(); + + // Wait for request to load + await page.waitForTimeout(1000); + + // Verify the variable is visible in the request body (use .first() to avoid strict mode violation) + await expect(page.getByText('{{multiline_data_json}}').first()).toBeVisible(); + + // Step 6: Execute request and verify multiline JSON parsing + await page.locator('#send-request').getByRole('img').nth(2).click(); + + // Wait for response and verify success + await expect(page.getByText('200')).toBeVisible({ timeout: 15000 }); + + // Wait for response body to load + await page.waitForTimeout(2000); + + // Verify that the multiline JSON was properly sent and echoed back + // The response should contain the JSON values we sent (use .first() to avoid strict mode violations) + await expect(page.getByText('John Doe').first()).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('john@example.com').first()).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('dark').first()).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('2025-09-03').first()).toBeVisible({ timeout: 5000 }); + + // Also verify the structure was preserved (look for JSON structure indicators) + await expect(page.getByText('user').first()).toBeVisible(); + await expect(page.getByText('preferences').first()).toBeVisible(); + await expect(page.getByText('metadata').first()).toBeVisible(); + + // Cleanup: Remove the multiline_data_json variable from the Test.bru file + // This is much more reliable than trying to navigate the UI + }); + + // After the test, clean up the file + 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/e2e-tests/environment-multiline-variables/collection/bruno.json b/e2e-tests/environment-multiline-variables/collection/bruno.json new file mode 100644 index 0000000000..b2f6c48cda --- /dev/null +++ b/e2e-tests/environment-multiline-variables/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "multiline-variables", + "type": "collection" +} diff --git a/e2e-tests/environment-multiline-variables/collection/collection.bru b/e2e-tests/environment-multiline-variables/collection/collection.bru new file mode 100644 index 0000000000..aee0d18468 --- /dev/null +++ b/e2e-tests/environment-multiline-variables/collection/collection.bru @@ -0,0 +1,5 @@ +meta { + name: multiline-variables + type: collection + version: 1.0.0 +} diff --git a/e2e-tests/environment-multiline-variables/collection/environments/Test.bru b/e2e-tests/environment-multiline-variables/collection/environments/Test.bru new file mode 100644 index 0000000000..85d00f1eba --- /dev/null +++ b/e2e-tests/environment-multiline-variables/collection/environments/Test.bru @@ -0,0 +1,8 @@ +vars { + host: https://www.httpfaker.org + multiline_data: ''' + line1: first line of data + line2: second line of data + line3: third line of data + ''' +} diff --git a/e2e-tests/environment-multiline-variables/collection/multiline-test.bru b/e2e-tests/environment-multiline-variables/collection/multiline-test.bru new file mode 100644 index 0000000000..2e94de6580 --- /dev/null +++ b/e2e-tests/environment-multiline-variables/collection/multiline-test.bru @@ -0,0 +1,21 @@ +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); + }); +} diff --git a/e2e-tests/environment-multiline-variables/collection/ping-request.bru b/e2e-tests/environment-multiline-variables/collection/ping-request.bru new file mode 100644 index 0000000000..f9510d7fe6 --- /dev/null +++ b/e2e-tests/environment-multiline-variables/collection/ping-request.bru @@ -0,0 +1,24 @@ +meta { + name: ping-request + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo + body: json + auth: none +} + +body:json { + { + "message": "ping test", + "host_variable": "{{host}}" + } +} + +tests { + test("should get 200 response", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/e2e-tests/environment-multiline-variables/init-user-data/preferences.json b/e2e-tests/environment-multiline-variables/init-user-data/preferences.json new file mode 100644 index 0000000000..13e390de24 --- /dev/null +++ b/e2e-tests/environment-multiline-variables/init-user-data/preferences.json @@ -0,0 +1,28 @@ +{ + "maximized": true, + "lastOpenedCollections": [ + "{{projectRoot}}/e2e-tests/environment-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 From 8dc33eefa63979eb0cb33f603e939a2ddcb727c1 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 3 Sep 2025 21:20:41 +0545 Subject: [PATCH 14/23] chore: remove obsolete bruMultiline tests - Deleted the `bruMultiline.spec.js` file as it is no longer needed. - This cleanup helps maintain a lean test suite and reduces redundancy. --- .../bruno-lang/v2/tests/bruMultiline.spec.js | 270 ------------------ 1 file changed, 270 deletions(-) delete mode 100644 packages/bruno-lang/v2/tests/bruMultiline.spec.js diff --git a/packages/bruno-lang/v2/tests/bruMultiline.spec.js b/packages/bruno-lang/v2/tests/bruMultiline.spec.js deleted file mode 100644 index 023e0f254f..0000000000 --- a/packages/bruno-lang/v2/tests/bruMultiline.spec.js +++ /dev/null @@ -1,270 +0,0 @@ -const parser = require('../src/bruToJson.js'); - -describe('bru parser multiline', () => { - it('should parse multiline body:json with proper indentation', () => { - const input = ` -meta { - name: test -} - -post { - url: https://example.com -} - -body:json { - ''' - { - "name": "test", - "value": 123 - } - ''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'post', - url: 'https://example.com' - }, - body: { - json: "\'\'\'\n{\n \"name\": \"test\",\n \"value\": 123\n}\n\'\'\'" - } - }; - expect(output).toEqual(expected); - }); - - it('should parse multiline body:text with proper indentation', () => { - const input = ` -meta { - name: test -} - -post { - url: https://example.com -} - -body:text { - ''' - Hello World - This is a multiline - text content - ''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'post', - url: 'https://example.com' - }, - body: { - text: "\'\'\'\nHello World\nThis is a multiline\ntext content\n\'\'\'" - } - }; - expect(output).toEqual(expected); - }); - - it('should parse multiline script:pre-request with proper indentation', () => { - const input = ` -meta { - name: test -} - -get { - url: https://example.com -} - -script:pre-request { - ''' - console.log('Starting request'); - bru.setVar('timestamp', Date.now()); - ''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'get', - url: 'https://example.com' - }, - script: { - req: "\'\'\'\nconsole.log('Starting request');\nbru.setVar('timestamp', Date.now());\n\'\'\'" - } - }; - expect(output).toEqual(expected); - }); - - it('should parse multiline tests with proper indentation', () => { - const input = ` -meta { - name: test -} - -get { - url: https://example.com -} - -tests { - ''' - test('Status code is 200', function() { - expect(res.getStatus()).to.equal(200); - }); - - test('Response has data', function() { - expect(res.getBody()).to.have.property('data'); - }); - ''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'get', - url: 'https://example.com' - }, - tests: "\'\'\'\ntest('Status code is 200', function() {\n expect(res.getStatus()).to.equal(200);\n});\n\ntest('Response has data', function() {\n expect(res.getBody()).to.have.property('data');\n});\n\'\'\'" - }; - expect(output).toEqual(expected); - }); - - it('should NOT treat single-line content with triple quotes as multiline', () => { - const input = ` -meta { - name: test -} - -post { - url: https://example.com -} - -body:json { - '''{"name": "test"}''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'post', - url: 'https://example.com' - }, - body: { - json: '\'\'\'{"name": "test"}\'\'\'' - } - }; - expect(output).toEqual(expected); - }); - - it('should handle mixed content in body:json', () => { - const input = ` -meta { - name: test -} - -post { - url: https://example.com -} - -body:json { - { - "name": "test", - "nested": { - "value": 123 - } - } -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'post', - url: 'https://example.com' - }, - body: { - json: '{\n "name": "test",\n "nested": {\n "value": 123\n }\n}' - } - }; - expect(output).toEqual(expected); - }); - - it('should parse multiline dictionary values with proper indentation', () => { - const input = ` -meta { - name: test -} - -post { - url: https://example.com -} - -headers { - content-type: application/json - custom-header: ''' - This is a multiline - header value - ''' -} -`; - - const output = parser(input); - const expected = { - meta: { - name: 'test', - seq: 1, - type: 'http' - }, - http: { - method: 'post', - url: 'https://example.com' - }, - headers: [ - { - name: 'content-type', - value: 'application/json', - enabled: true - }, - { - name: 'custom-header', - value: 'is is a multiline\nader value', - enabled: true - } - ] - }; - expect(output).toEqual(expected); - }); -}); From 65cdef47ab036ede7766d10f96a38fe7fda53816 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 3 Sep 2025 22:56:37 +0545 Subject: [PATCH 15/23] tests: refactor playwright testcases --- .../001-read-multiline-environment.spec.ts | 65 -------- ...reate-and-parse-multiline-variable.spec.ts | 149 ------------------ .../collection/multiline-test.bru | 21 --- .../collection/ping-request.bru | 24 --- .../001-read-multiline-environment.spec.ts | 50 ++++++ ...reate-and-parse-multiline-variable.spec.ts | 111 +++++++++++++ .../collection/bruno.json | 0 .../collection/collection.bru | 2 +- .../collection/environments/Test.bru | 0 .../collection/multiline-test.bru | 44 ++++++ .../collection/ping-request.bru | 56 +++++++ .../init-user-data/preferences.json | 2 +- 12 files changed, 263 insertions(+), 261 deletions(-) delete mode 100644 e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts delete mode 100644 e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts delete mode 100644 e2e-tests/environment-multiline-variables/collection/multiline-test.bru delete mode 100644 e2e-tests/environment-multiline-variables/collection/ping-request.bru create mode 100644 e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts create mode 100644 e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts rename e2e-tests/{environment-multiline-variables => environments/multiline-variables}/collection/bruno.json (100%) rename e2e-tests/{environment-multiline-variables => environments/multiline-variables}/collection/collection.bru (97%) rename e2e-tests/{environment-multiline-variables => environments/multiline-variables}/collection/environments/Test.bru (100%) create mode 100644 e2e-tests/environments/multiline-variables/collection/multiline-test.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/ping-request.bru rename e2e-tests/{environment-multiline-variables => environments/multiline-variables}/init-user-data/preferences.json (85%) diff --git a/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts b/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts deleted file mode 100644 index e3bed2f6ee..0000000000 --- a/e2e-tests/environment-multiline-variables/001-read-multiline-environment.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { test, expect } from '../../playwright'; - -test.describe('Multiline Variables - Read Environment Test', () => { - test('should read existing multiline environment variables and execute request', async ({ pageWithUserData: page }) => { - test.setTimeout(30 * 1000); - - // Step 1: Initialize collection (handle both fresh and configured states) - await page.locator('#sidebar-collection-name').click(); - - // Wait for any dialogs to appear and handle them - await page.waitForTimeout(1500); - - // Handle collection security dialog if it appears - const safeModeLabel = page.getByLabel('Safe Mode'); - const safeModeRadio = page.getByRole('radio', { name: 'Safe Mode' }); - const saveButton = page.getByRole('button', { name: 'Save' }); - - if (await safeModeLabel.isVisible()) { - await safeModeLabel.check(); - await saveButton.click(); - } else if (await safeModeRadio.isVisible()) { - await safeModeRadio.click(); - await saveButton.click(); - } - // If neither is visible, collection is already configured - - // Step 2: Navigate to ping-request - await page.getByText('ping-request', { exact: true }).click(); - - // Step 3: Ensure Test environment is selected - // Handle multiple possible states: "No Environment", "Test", or already configured - const noEnvButton = page.getByText('No Environment'); - const testEnvButton = page.locator('.current-environment').filter({ hasText: /Test/ }); - - if (await noEnvButton.isVisible()) { - // Environment not set, select Test - await noEnvButton.click(); - await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); - } else if (await testEnvButton.isVisible()) { - // Test environment already selected, continue - console.log('Test environment already selected'); - } else { - // Try to find and select Test environment from dropdown - try { - await page.locator('.environment-selector').click({ timeout: 2000 }); - await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); - } catch { - console.log('Environment setup complete'); - } - } - - // Step 4: Execute request to verify multiline environment variables work - await page.locator('#send-request').getByRole('img').nth(2).click(); - - // Verify successful response (confirms multiline env vars are parsed correctly) - await expect(page.getByText('200')).toBeVisible({ timeout: 15000 }); - - // Additional verification: check that host variable was resolved in the response - await page.waitForTimeout(2000); - - // Verify the response contains the resolved host variable value - // Use main role to target response area specifically (avoids request panel duplication) - await expect(page.getByRole('main')).toContainText('httpfaker.org', { timeout: 5000 }); - }); -}); diff --git a/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts b/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts deleted file mode 100644 index 9d3a3def94..0000000000 --- a/e2e-tests/environment-multiline-variables/002-create-and-parse-multiline-variable.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { test, expect } from '../../playwright'; - -test.describe('Multiline Variables - Create and Parse Test', () => { - test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => { - test.setTimeout(60 * 1000); - - // Step 1: Initialize collection (handle both fresh and configured states) - await page.locator('#sidebar-collection-name').click(); - - // Wait for any dialogs to appear and handle them - await page.waitForTimeout(1500); - - // Handle collection security dialog if it appears - const safeModeLabel = page.getByLabel('Safe Mode'); - const safeModeRadio = page.getByRole('radio', { name: 'Safe Mode' }); - const saveButton = page.getByRole('button', { name: 'Save' }); - - if (await safeModeLabel.isVisible()) { - await safeModeLabel.check(); - await saveButton.click(); - } else if (await safeModeRadio.isVisible()) { - await safeModeRadio.click(); - await saveButton.click(); - } - // If neither is visible, collection is already configured - - // Step 2: Handle environment selection and setup (from main collection view) - // Ensure Test environment is selected before configuring variables - const noEnvButton = page.getByText('No Environment'); - const testEnvButton = page.locator('.current-environment').filter({ hasText: /Test/ }); - - if (await noEnvButton.isVisible()) { - // Environment not set, select Test - await noEnvButton.click(); - await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); - } else if (await testEnvButton.isVisible()) { - // Test environment already selected, continue - console.log('Test environment already selected'); - } else { - // Try to find and select Test environment from dropdown - try { - await page.locator('.environment-selector').click({ timeout: 2000 }); - await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); - } catch { - console.log('Environment setup complete'); - } - } - - // Step 3: Open environment configuration to create the multiline variable - // Handle different ways the environment dropdown might appear - try { - await page.locator('.current-environment').filter({ hasText: /Test/ }).click(); - await page.getByText('Configure', { exact: true }).click(); - } catch { - // Alternative approach if the above fails - try { - await page.locator('.environment-selector').click(); - await page.getByText('Configure', { exact: true }).click(); - } catch { - // If configure is already visible, just click it - await page.getByText('Configure', { exact: true }).click(); - } - } - - // Step 4: Create the multiline environment variable - await page.getByRole('button', { name: /Add.*Variable/i }).click(); - - // Wait for the form to appear - await page.waitForTimeout(1000); - - // Add multiline JSON value first - const jsonValue = `{ - "user": { - "name": "John Doe", - "email": "john@example.com", - "preferences": { - "theme": "dark", - "notifications": true - } - }, - "metadata": { - "created": "2025-09-03", - "version": "1.0" - } -}`; - - // Fill the value textarea first (we know this works) - const valueInput = page.locator('textarea').last(); - await valueInput.fill(jsonValue); - - // Navigate backwards to the name input using Shift+Tab - await page.keyboard.press('Shift+Tab'); - await page.keyboard.type('multiline_data_json'); - - // Save the environment variable - await page.getByRole('button', { name: /Save/i }).click(); - - // Close environment config - await page.getByText('×').click(); - - // Step 5: Navigate to multiline-test request and verify variable usage - // (The request is already configured to use {{multiline_data_json}}) - await page.getByText('multiline-test', { exact: true }).click(); - - // Wait for request to load - await page.waitForTimeout(1000); - - // Verify the variable is visible in the request body (use .first() to avoid strict mode violation) - await expect(page.getByText('{{multiline_data_json}}').first()).toBeVisible(); - - // Step 6: Execute request and verify multiline JSON parsing - await page.locator('#send-request').getByRole('img').nth(2).click(); - - // Wait for response and verify success - await expect(page.getByText('200')).toBeVisible({ timeout: 15000 }); - - // Wait for response body to load - await page.waitForTimeout(2000); - - // Verify that the multiline JSON was properly sent and echoed back - // The response should contain the JSON values we sent (use .first() to avoid strict mode violations) - await expect(page.getByText('John Doe').first()).toBeVisible({ timeout: 5000 }); - await expect(page.getByText('john@example.com').first()).toBeVisible({ timeout: 5000 }); - await expect(page.getByText('dark').first()).toBeVisible({ timeout: 5000 }); - await expect(page.getByText('2025-09-03').first()).toBeVisible({ timeout: 5000 }); - - // Also verify the structure was preserved (look for JSON structure indicators) - await expect(page.getByText('user').first()).toBeVisible(); - await expect(page.getByText('preferences').first()).toBeVisible(); - await expect(page.getByText('metadata').first()).toBeVisible(); - - // Cleanup: Remove the multiline_data_json variable from the Test.bru file - // This is much more reliable than trying to navigate the UI - }); - - // After the test, clean up the file - 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/e2e-tests/environment-multiline-variables/collection/multiline-test.bru b/e2e-tests/environment-multiline-variables/collection/multiline-test.bru deleted file mode 100644 index 2e94de6580..0000000000 --- a/e2e-tests/environment-multiline-variables/collection/multiline-test.bru +++ /dev/null @@ -1,21 +0,0 @@ -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); - }); -} diff --git a/e2e-tests/environment-multiline-variables/collection/ping-request.bru b/e2e-tests/environment-multiline-variables/collection/ping-request.bru deleted file mode 100644 index f9510d7fe6..0000000000 --- a/e2e-tests/environment-multiline-variables/collection/ping-request.bru +++ /dev/null @@ -1,24 +0,0 @@ -meta { - name: ping-request - type: http - seq: 1 -} - -post { - url: {{host}}/api/echo - body: json - auth: none -} - -body:json { - { - "message": "ping test", - "host_variable": "{{host}}" - } -} - -tests { - test("should get 200 response", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts b/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts new file mode 100644 index 0000000000..42b6e30396 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Multiline Variables - Read Environment Test', () => { + test('should read existing multiline environment variables and execute request', async ({ pageWithUserData: page }) => { + test.setTimeout(30 * 1000); + + // Step 1: Wait for collection to be loaded and click it + await expect(page.locator('#sidebar-collection-name')).toBeVisible(); + await page.locator('#sidebar-collection-name').click(); + + // Step 2: Handle collection dialog (following proven pattern from persistent-env-tests) + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Step 3: Wait for collection to be ready and navigate to ping-request + await expect(page.getByText('ping-request', { exact: true })).toBeVisible(); + await page.getByText('ping-request', { exact: true }).click(); + + // Step 4: Wait for request page to load and select Test environment + await expect(page.getByText('No Environment')).toBeVisible(); + await page.getByText('No Environment').click(); + + // Step 5: Wait for environment dropdown and select Test + await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + + // Step 6: Wait for Test environment to be selected + await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); + + // Step 7: Wait for send button and execute request + const sendButton = page.locator('#send-request').getByRole('img').nth(2); + await expect(sendButton).toBeVisible(); + await sendButton.click(); + + // Step 8: Wait for response status + await expect(page.getByText('200')).toBeVisible(); + + // Step 9: Verify environment variable resolution in response content + await expect(page.getByRole('main')).toContainText('httpfaker.org'); + await expect(page.getByRole('main')).toContainText('Host: https://www.httpfaker.org'); + await expect(page.getByRole('main')).toContainText('Ping Test Request'); + + // Step 10: Verify multiline environment variable resolution + // The multiline_data variable should be resolved and contain all three lines + await expect(page.getByRole('main')).toContainText('line1: first line of data'); + await expect(page.getByRole('main')).toContainText('line2: second line of data'); + await expect(page.getByRole('main')).toContainText('line3: third line of data'); + await expect(page.getByRole('main')).toContainText('Multiline Data:'); + }); +}); \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts b/e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts new file mode 100644 index 0000000000..49559f2e41 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Multiline Variables - Create and Parse Test', () => { + test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => { + test.setTimeout(60 * 1000); + + // Step 1: Wait for collection to be loaded and click it + await expect(page.locator('#sidebar-collection-name')).toBeVisible(); + await page.locator('#sidebar-collection-name').click(); + + // Step 2: Collection is already configured from previous test - no dialog appears + // Wait for collection to be ready by checking if requests are visible + await expect(page.getByText('multiline-test', { exact: true })).toBeVisible(); + + // Step 3: Check environment state (Test environment should already be selected) + + // Step 4: Handle environment selection based on current state + const noEnvButton = page.getByText('No Environment'); + const currentEnv = page.locator('.current-environment').filter({ hasText: /Test/ }); + + // Wait for either "No Environment" or current environment to be visible + await expect(noEnvButton.or(currentEnv)).toBeVisible(); + + // If "No Environment" is visible, select Test environment + if (await noEnvButton.isVisible()) { + await noEnvButton.click(); + await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + } + + // Step 5: Wait for Test environment to be active + await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); + + // Step 6: Open environment configuration + await page.locator('.current-environment').filter({ hasText: /Test/ }).click(); + await expect(page.getByText('Configure', { exact: true })).toBeVisible(); + await page.getByText('Configure', { exact: true }).click(); + + // Step 7: Wait for environment config dialog and add variable + await expect(page.getByRole('button', { name: /Add.*Variable/i })).toBeVisible(); + await page.getByRole('button', { name: /Add.*Variable/i }).click(); + + // Step 8: Wait for variable form and fill it + const valueTextarea = page.locator('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 form + await valueTextarea.fill(jsonValue); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.type('multiline_data_json'); + + // Step 9: Save variable and close config + const saveVarButton = page.getByRole('button', { name: /Save/i }); + await expect(saveVarButton).toBeVisible(); + await saveVarButton.click(); + + await expect(page.getByText('×')).toBeVisible(); + await page.getByText('×').click(); + + // Step 10: Navigate to multiline-test request + await expect(page.getByText('multiline-test', { exact: true })).toBeVisible(); + await page.getByText('multiline-test', { exact: true }).click(); + + // Step 11: Verify variable is visible in request body + await expect(page.getByText('{{multiline_data_json}}').first()).toBeVisible(); + + // Step 12: Execute request + const sendButton = page.locator('#send-request').getByRole('img').nth(2); + await expect(sendButton).toBeVisible(); + await sendButton.click(); + + // Step 13: Wait for response status + await expect(page.getByText('200')).toBeVisible(); + + // Step 14: Verify multiline JSON variable resolution in response + await expect(page.getByRole('main')).toContainText('John Doe'); + await expect(page.getByRole('main')).toContainText('john@example.com'); + await expect(page.getByRole('main')).toContainText('dark'); + await expect(page.getByRole('main')).toContainText('2025-09-03'); + await expect(page.getByRole('main')).toContainText('httpfaker.org'); + }); + + // 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); + }); +}); \ No newline at end of file diff --git a/e2e-tests/environment-multiline-variables/collection/bruno.json b/e2e-tests/environments/multiline-variables/collection/bruno.json similarity index 100% rename from e2e-tests/environment-multiline-variables/collection/bruno.json rename to e2e-tests/environments/multiline-variables/collection/bruno.json diff --git a/e2e-tests/environment-multiline-variables/collection/collection.bru b/e2e-tests/environments/multiline-variables/collection/collection.bru similarity index 97% rename from e2e-tests/environment-multiline-variables/collection/collection.bru rename to e2e-tests/environments/multiline-variables/collection/collection.bru index aee0d18468..66d9f88e40 100644 --- a/e2e-tests/environment-multiline-variables/collection/collection.bru +++ b/e2e-tests/environments/multiline-variables/collection/collection.bru @@ -2,4 +2,4 @@ meta { name: multiline-variables type: collection version: 1.0.0 -} +} \ No newline at end of file diff --git a/e2e-tests/environment-multiline-variables/collection/environments/Test.bru b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru similarity index 100% rename from e2e-tests/environment-multiline-variables/collection/environments/Test.bru rename to e2e-tests/environments/multiline-variables/collection/environments/Test.bru 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 0000000000..4f19735c90 --- /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/ping-request.bru b/e2e-tests/environments/multiline-variables/collection/ping-request.bru new file mode 100644 index 0000000000..d9fd8bf5a3 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/ping-request.bru @@ -0,0 +1,56 @@ +meta { + name: ping-request + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo + body: text + auth: none +} + +body:text { + Ping Test Request + Host: {{host}} + + Multiline Data: + {{multiline_data}} + + End of multiline content. +} + +tests { + test("should get 200 response", function() { + expect(res.getStatus()).to.equal(200); + }); + + test("should resolve host environment variable correctly", function() { + const body = res.getBody(); + // HTTP Faker echo endpoint returns the request details + // Verify the host variable was resolved in the request URL + expect(body.url).to.contain("httpfaker.org/api/echo"); + }); + + test("should resolve host variable in request body", function() { + const body = res.getBody(); + // Verify the host variable was resolved in the text body + expect(body.body).to.contain("Host: https://www.httpfaker.org"); + }); + + test("should contain static content in request body", function() { + const body = res.getBody(); + // Verify static content is preserved + expect(body.body).to.contain("Ping Test Request"); + expect(body.body).to.contain("End of multiline content"); + }); + + 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.contain("line1: first line of data"); + expect(body.body).to.contain("line2: second line of data"); + expect(body.body).to.contain("line3: third line of data"); + expect(body.body).to.contain("Multiline Data:"); + }); +} diff --git a/e2e-tests/environment-multiline-variables/init-user-data/preferences.json b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json similarity index 85% rename from e2e-tests/environment-multiline-variables/init-user-data/preferences.json rename to e2e-tests/environments/multiline-variables/init-user-data/preferences.json index 13e390de24..9374c16fd7 100644 --- a/e2e-tests/environment-multiline-variables/init-user-data/preferences.json +++ b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json @@ -1,7 +1,7 @@ { "maximized": true, "lastOpenedCollections": [ - "{{projectRoot}}/e2e-tests/environment-multiline-variables/collection" + "{{projectRoot}}/e2e-tests/environments/multiline-variables/collection" ], "request": { "sslVerification": false, From 011832f1b888f044052bb93d6045994f7826b2bd Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Thu, 4 Sep 2025 17:27:02 +0530 Subject: [PATCH 16/23] test: update test descriptions for clarity --- .../bruno-lang/v2/tests/envToJson.spec.js | 2 +- .../bruno-lang/v2/tests/jsonToEnv.spec.js | 41 +++++-------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js index a6352a6144..9c19dc211a 100644 --- a/packages/bruno-lang/v2/tests/envToJson.spec.js +++ b/packages/bruno-lang/v2/tests/envToJson.spec.js @@ -340,7 +340,7 @@ vars { expect(output).toEqual(expected); }); - it('should parse multiline variable with mixed indentation', () => { + it('should parse multiline variable that has indentation', () => { const input = ` vars { script: ''' diff --git a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js index 90193f41c8..940603094e 100644 --- a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js +++ b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js @@ -1,7 +1,7 @@ const parser = require('../src/jsonToEnv'); -describe('env parser', () => { - it('should parse empty vars', () => { +describe('jsonToEnv', () => { + it('should stringify empty vars', () => { const input = { variables: [] }; @@ -14,7 +14,7 @@ describe('env parser', () => { expect(output).toEqual(expected); }); - it('should parse single var line', () => { + it('should stringify single var line', () => { const input = { variables: [ { @@ -33,7 +33,7 @@ describe('env parser', () => { expect(output).toEqual(expected); }); - it('should parse multiple var lines', () => { + it('should stringify multiple var lines', () => { const input = { variables: [ { @@ -58,7 +58,7 @@ describe('env parser', () => { expect(output).toEqual(expected); }); - it('should parse secret vars', () => { + it('should stringify secret vars', () => { const input = { variables: [ { @@ -86,7 +86,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should parse multiple secret vars', () => { + it('should stringify multiple secret vars', () => { const input = { variables: [ { @@ -121,7 +121,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should parse even if the only secret vars are present', () => { + it('should stringify even if the only secret vars are present', () => { const input = { variables: [ { @@ -141,7 +141,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should generate multiline variable values', () => { + it('should stringify multiline variables', () => { const input = { variables: [ { @@ -165,7 +165,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should generate multiline variable with proper indentation', () => { + it('should stringify multiline variables containing indentation', () => { const input = { variables: [ { @@ -189,7 +189,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should generate disabled multiline variable', () => { + it('should stringify disabled multiline variable', () => { const input = { variables: [ { @@ -212,7 +212,7 @@ vars:secret [ expect(output).toEqual(expected); }); - it('should generate multiple multiline variables', () => { + it('should stringify multiple multiline variables', () => { const input = { variables: [ { @@ -240,25 +240,6 @@ vars:secret [ ''' } -`; - expect(output).toEqual(expected); - }); - - it('should handle single line values normally', () => { - const input = { - variables: [ - { - name: 'simple', - value: 'single line value', - enabled: true - } - ] - }; - - const output = parser(input); - const expected = `vars { - simple: single line value -} `; expect(output).toEqual(expected); }); From 728f84fdce1ad972e7cd6c624f6bb33d3576177d Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Thu, 4 Sep 2025 18:28:15 +0530 Subject: [PATCH 17/23] test: refactor multiline environment variable tests --- .../001-read-multiline-environment.spec.ts | 50 ----------------- .../collection/environments/Test.bru | 6 +- .../collection/ping-request.bru | 56 ------------------- .../collection/request.bru | 38 +++++++++++++ .../init-user-data/collection-security.json | 10 ++++ .../read-multiline-environment.spec.ts | 34 +++++++++++ ...ec.ts => write-multiline-variable.spec.ts} | 0 .../ResponsePane/StatusCode/index.js | 2 +- .../Sidebar/Collections/Collection/index.js | 2 +- 9 files changed, 87 insertions(+), 111 deletions(-) delete mode 100644 e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts delete mode 100644 e2e-tests/environments/multiline-variables/collection/ping-request.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/read-multiline-environment.spec.ts rename e2e-tests/environments/multiline-variables/{002-create-and-parse-multiline-variable.spec.ts => write-multiline-variable.spec.ts} (100%) diff --git a/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts b/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts deleted file mode 100644 index 42b6e30396..0000000000 --- a/e2e-tests/environments/multiline-variables/001-read-multiline-environment.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { test, expect } from '../../../playwright'; - -test.describe('Multiline Variables - Read Environment Test', () => { - test('should read existing multiline environment variables and execute request', async ({ pageWithUserData: page }) => { - test.setTimeout(30 * 1000); - - // Step 1: Wait for collection to be loaded and click it - await expect(page.locator('#sidebar-collection-name')).toBeVisible(); - await page.locator('#sidebar-collection-name').click(); - - // Step 2: Handle collection dialog (following proven pattern from persistent-env-tests) - await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - - // Step 3: Wait for collection to be ready and navigate to ping-request - await expect(page.getByText('ping-request', { exact: true })).toBeVisible(); - await page.getByText('ping-request', { exact: true }).click(); - - // Step 4: Wait for request page to load and select Test environment - await expect(page.getByText('No Environment')).toBeVisible(); - await page.getByText('No Environment').click(); - - // Step 5: Wait for environment dropdown and select Test - await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); - await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); - - // Step 6: Wait for Test environment to be selected - await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); - - // Step 7: Wait for send button and execute request - const sendButton = page.locator('#send-request').getByRole('img').nth(2); - await expect(sendButton).toBeVisible(); - await sendButton.click(); - - // Step 8: Wait for response status - await expect(page.getByText('200')).toBeVisible(); - - // Step 9: Verify environment variable resolution in response content - await expect(page.getByRole('main')).toContainText('httpfaker.org'); - await expect(page.getByRole('main')).toContainText('Host: https://www.httpfaker.org'); - await expect(page.getByRole('main')).toContainText('Ping Test Request'); - - // Step 10: Verify multiline environment variable resolution - // The multiline_data variable should be resolved and contain all three lines - await expect(page.getByRole('main')).toContainText('line1: first line of data'); - await expect(page.getByRole('main')).toContainText('line2: second line of data'); - await expect(page.getByRole('main')).toContainText('line3: third line of data'); - await expect(page.getByRole('main')).toContainText('Multiline Data:'); - }); -}); \ 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 index 85d00f1eba..7a9190cd1a 100644 --- a/e2e-tests/environments/multiline-variables/collection/environments/Test.bru +++ b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru @@ -1,8 +1,8 @@ vars { host: https://www.httpfaker.org multiline_data: ''' - line1: first line of data - line2: second line of data - line3: third line of data + line1 + line2 + line3 ''' } diff --git a/e2e-tests/environments/multiline-variables/collection/ping-request.bru b/e2e-tests/environments/multiline-variables/collection/ping-request.bru deleted file mode 100644 index d9fd8bf5a3..0000000000 --- a/e2e-tests/environments/multiline-variables/collection/ping-request.bru +++ /dev/null @@ -1,56 +0,0 @@ -meta { - name: ping-request - type: http - seq: 1 -} - -post { - url: {{host}}/api/echo - body: text - auth: none -} - -body:text { - Ping Test Request - Host: {{host}} - - Multiline Data: - {{multiline_data}} - - End of multiline content. -} - -tests { - test("should get 200 response", function() { - expect(res.getStatus()).to.equal(200); - }); - - test("should resolve host environment variable correctly", function() { - const body = res.getBody(); - // HTTP Faker echo endpoint returns the request details - // Verify the host variable was resolved in the request URL - expect(body.url).to.contain("httpfaker.org/api/echo"); - }); - - test("should resolve host variable in request body", function() { - const body = res.getBody(); - // Verify the host variable was resolved in the text body - expect(body.body).to.contain("Host: https://www.httpfaker.org"); - }); - - test("should contain static content in request body", function() { - const body = res.getBody(); - // Verify static content is preserved - expect(body.body).to.contain("Ping Test Request"); - expect(body.body).to.contain("End of multiline content"); - }); - - 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.contain("line1: first line of data"); - expect(body.body).to.contain("line2: second line of data"); - expect(body.body).to.contain("line3: third line of data"); - expect(body.body).to.contain("Multiline Data:"); - }); -} 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 0000000000..1f82b09507 --- /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 0000000000..5f51bd286b --- /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/read-multiline-environment.spec.ts b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts new file mode 100644 index 0000000000..adad646421 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts @@ -0,0 +1,34 @@ +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.getByText('request', { exact: true })).toBeVisible(); + await page.getByText('request', { exact: true }).click(); + + // open environment dropdown + await expect(page.getByText('No Environment')).toBeVisible(); + await page.getByText('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.getByText('200')).toBeVisible(); + + // response pane should contain the expected multiline text + const responsePane = page.locator('div.response-pane CodeMirror'); + await expect(responsePane).toContainText('https://www.httpfaker.org\nline1\nline2\nline3'); + }); +}); \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts b/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts similarity index 100% rename from e2e-tests/environments/multiline-variables/002-create-and-parse-multiline-variable.spec.ts rename to e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts diff --git a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js index 9287899b1e..37f673636d 100644 --- a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js +++ b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js @@ -16,7 +16,7 @@ const StatusCode = ({ status }) => { }; 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 0f44b467a2..67ffcb68b7 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} /> -