From 7efaa427cabca837515a191b5eab98b19c4351ce Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 1 Aug 2025 13:50:13 +0530 Subject: [PATCH 01/18] feat: script to calculate locs of repo --- scripts/count-locs.js | 128 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 scripts/count-locs.js diff --git a/scripts/count-locs.js b/scripts/count-locs.js new file mode 100755 index 000000000..2e12afac8 --- /dev/null +++ b/scripts/count-locs.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const PACKAGES_DIR = path.join(__dirname, '..', 'packages'); +const EXCLUDE_DIRS = ['node_modules', 'dist', 'build', '.next', 'coverage', '.git']; +const EXCLUDE_PACKAGES = ['bruno-toml', 'bruno-tests', 'bruno-docs']; +const CODE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.json', '.md']; + +function countLinesInFile(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + return content.split('\n').length; + } catch (error) { + return 0; + } +} + +function shouldExcludeDir(dirName) { + return EXCLUDE_DIRS.includes(dirName) || dirName.startsWith('.'); +} + +function isCodeFile(fileName) { + return CODE_EXTENSIONS.some(ext => fileName.endsWith(ext)); +} + +function countLinesInDirectory(dirPath) { + let totalLines = 0; + let fileCount = 0; + + function walkDir(currentPath) { + const items = fs.readdirSync(currentPath); + + for (const item of items) { + const itemPath = path.join(currentPath, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + if (!shouldExcludeDir(item)) { + walkDir(itemPath); + } + } else if (stat.isFile() && isCodeFile(item)) { + const lines = countLinesInFile(itemPath); + totalLines += lines; + fileCount++; + } + } + } + + walkDir(dirPath); + return { totalLines, fileCount }; +} + +function getPackages() { + const packages = []; + const items = fs.readdirSync(PACKAGES_DIR); + + for (const item of items) { + const itemPath = path.join(PACKAGES_DIR, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory() && !shouldExcludeDir(item) && !EXCLUDE_PACKAGES.includes(item)) { + packages.push({ + name: item, + path: itemPath + }); + } + } + + return packages; +} + +function formatNumber(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +function printTable(data) { + // Calculate column widths + const nameWidth = Math.max(20, ...data.map(d => d.name.length)); + const locWidth = 12; + const filesWidth = 12; + + // Header + console.log('\n┌' + '─'.repeat(nameWidth + 2) + '┬' + '─'.repeat(locWidth + 2) + '┬' + '─'.repeat(filesWidth + 2) + '┐'); + console.log(`│ ${'Package'.padEnd(nameWidth)} │ ${'LOC'.padStart(locWidth)} │ ${'Files'.padStart(filesWidth)} │`); + console.log('├' + '─'.repeat(nameWidth + 2) + '┼' + '─'.repeat(locWidth + 2) + '┼' + '─'.repeat(filesWidth + 2) + '┤'); + + // Data rows + let totalLOC = 0; + let totalFiles = 0; + + for (const row of data) { + console.log(`│ ${row.name.padEnd(nameWidth)} │ ${formatNumber(row.loc).padStart(locWidth)} │ ${formatNumber(row.files).padStart(filesWidth)} │`); + totalLOC += row.loc; + totalFiles += row.files; + } + + // Footer + console.log('├' + '─'.repeat(nameWidth + 2) + '┼' + '─'.repeat(locWidth + 2) + '┼' + '─'.repeat(filesWidth + 2) + '┤'); + console.log(`│ ${'TOTAL'.padEnd(nameWidth)} │ ${formatNumber(totalLOC).padStart(locWidth)} │ ${formatNumber(totalFiles).padStart(filesWidth)} │`); + console.log('└' + '─'.repeat(nameWidth + 2) + '┴' + '─'.repeat(locWidth + 2) + '┴' + '─'.repeat(filesWidth + 2) + '┘\n'); +} + +function main() { + console.log('Counting lines of code in Bruno packages...\n'); + + const packages = getPackages(); + const results = []; + + for (const pkg of packages) { + process.stdout.write(`Analyzing ${pkg.name}...`); + const { totalLines, fileCount } = countLinesInDirectory(pkg.path); + results.push({ + name: pkg.name, + loc: totalLines, + files: fileCount + }); + process.stdout.write(' Done\n'); + } + + // Sort by LOC descending + results.sort((a, b) => b.loc - a.loc); + + printTable(results); +} + +main(); \ No newline at end of file From da2f2519ece5751de643fc870fad2611c4031cfc Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Tue, 5 Aug 2025 17:17:38 +0530 Subject: [PATCH 02/18] feat: add Playwright testing guide for Bruno application --- docs/playwright-testing-guide.md | 470 +++++++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 docs/playwright-testing-guide.md diff --git a/docs/playwright-testing-guide.md b/docs/playwright-testing-guide.md new file mode 100644 index 000000000..c0de8ad3d --- /dev/null +++ b/docs/playwright-testing-guide.md @@ -0,0 +1,470 @@ +# Playwright Testing Guide for Bruno + +This guide explains how to create and run Playwright test cases for the Bruno application using the UI. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Creating Tests Using Codegen](#creating-tests-using-codegen) +- [Manual Test Creation](#manual-test-creation) +- [Test Structure and Organization](#test-structure-and-organization) +- [Available Test Fixtures](#available-test-fixtures) +- [Running Tests](#running-tests) +- [Best Practices](#best-practices) +- [Examples](#examples) +- [Troubleshooting](#troubleshooting) + +## Overview + +Bruno uses Playwright for end-to-end testing of its Electron application. The testing setup includes custom fixtures for Electron app testing and utilities for managing test data. + +## Prerequisites + +- Node.js installed +- All dependencies installed (`npm install`) +- Electron app can be built and run + +## Creating Tests Using Codegen + +The easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code. + +### Using the Built-in Codegen Script + +```bash +# Generate a test with a specific name +npm run test:codegen my-new-test + +# Generate a test without specifying a name (will prompt for input) +npm run test:codegen +``` + +### What Happens During Codegen + +1. The Electron app launches automatically +2. Playwright Inspector opens in a separate window +3. You interact with the Bruno UI +4. Actions are recorded and converted to test code +5. The generated test file is saved in `e2e-tests/` + +### Codegen Workflow + +1. **Start Recording**: Run the codegen command +2. **Interact with UI**: Perform the actions you want to test +3. **Add Assertions**: Use the inspector to add assertions +4. **Save Test**: The test file is automatically generated +5. **Review and Refine**: Edit the generated test as needed + +## Manual Test Creation + +You can also create tests manually by following the established patterns. + +### Basic Test Structure + +```typescript +import { test, expect } from '../../playwright'; + +test('Test description', async ({ page }) => { + // Test steps here + await page.getByLabel('Some Label').click(); + + // Assertions + await expect(page.getByText('Expected Text')).toBeVisible(); +}); +``` + +### Test with Temporary Data + +```typescript +import { test, expect } from '../../playwright'; + +test('Test with temporary data', async ({ page, createTmpDir }) => { + // Create temporary directory for test data + const testDir = await createTmpDir('test-collection'); + + // Test steps + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').fill('test-collection'); + await page.getByLabel('Location').fill(testDir); + + // Assertions + await expect(page.getByText('test-collection')).toBeVisible(); +}); +``` + +## Test Structure and Organization + +### Directory Structure + +``` +e2e-tests/ +├── 001-sanity-tests/ # Basic functionality tests +│ ├── 001-home-screen.spec.ts +│ └── 002-create-new-collection-and-new-request.spec.ts +├── 002-feature-tests/ # Specific feature tests +├── 003-integration-tests/ # Complex workflow tests +└── bruno-testbench/ # Test utilities and helpers +``` + +### Naming Conventions + +- **Files**: Use descriptive names with `.spec.ts` extension +- **Tests**: Use clear, descriptive test names +- **Folders**: Use numbered prefixes for ordering + +### Test File Template + +```typescript +import { test, expect } from '../../playwright'; + +test.describe('Feature Name', () => { + test('should perform specific action', async ({ page }) => { + // Arrange + // Act + // Assert + }); + + test('should handle error case', async ({ page }) => { + // Test error scenarios + }); +}); +``` + +## Available Test Fixtures + +The Bruno Playwright setup provides several custom fixtures: + +### Core Fixtures + +- `page`: Main page for testing +- `context`: Browser context +- `electronApp`: Electron application instance + +### Utility Fixtures + +- `createTmpDir`: Creates temporary directories for test data +- `newPage`: Creates a new page instance +- `pageWithUserData`: Page with custom user data +- `launchElectronApp`: Launches a new Electron app instance +- `reuseOrLaunchElectronApp`: Reuses existing app or launches new one + +### Using Fixtures + +```typescript +test('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => { + const testDir = await createTmpDir('test-data'); + + // Your test logic here +}); +``` + +## Running Tests + +### Basic Commands + +```bash +# Run all tests +npm run test:e2e + +# Run specific test file +npx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts + +# Run tests in a specific folder +npx playwright test e2e-tests/001-sanity-tests/ +``` + +### Advanced Options + +```bash +# Run with UI mode (for debugging) +npx playwright test --ui + +# Run in headed mode (see browser) +npx playwright test --headed + +# Run with specific browser +npx playwright test --project="Bruno Electron App" + +# Run with debugging +npx playwright test --debug + +# Run with trace recording +npx playwright test --trace on +``` + +### CI/CD Integration + +```bash +# Install browsers for CI +npx playwright install + +# Run tests in CI mode +npm run test:e2e +``` + +## Best Practices + +### 1. Use Semantic Selectors + +**Preferred:** + +```typescript +await page.getByRole('button', { name: 'Create' }).click(); +await page.getByLabel('Collection Name').fill('test'); +await page.getByText('Success message').toBeVisible(); +``` + +**Avoid:** + +```typescript +await page.locator('.btn-primary').click(); +await page.locator('#collection-name').fill('test'); +``` + +### 2. Create Isolated Tests + +Each test should be independent and not rely on other tests: + +```typescript +test('should create collection', async ({ page, createTmpDir }) => { + const testDir = await createTmpDir('collection-test'); + + // Test creates its own data + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').fill('test-collection'); + await page.getByLabel('Location').fill(testDir); + + // Clean up happens automatically via createTmpDir +}); +``` + +### 3. Add Meaningful Assertions + +Always verify the expected outcomes: + +```typescript +test('should save request successfully', async ({ page }) => { + // Arrange + await page.getByLabel('Create Collection').click(); + + // Act + await page.getByRole('button', { name: 'Save' }).click(); + + // Assert + await expect(page.getByText('Request saved successfully')).toBeVisible(); + await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible(); +}); +``` + +### 4. Handle Async Operations + +```typescript +test('should wait for network requests', async ({ page }) => { + // Wait for specific network request + await page.waitForResponse((response) => response.url().includes('/api/endpoint')); + + // Or wait for element to be stable + await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' }); +}); +``` + +### 5. Use Test Data Management + +```typescript +test('should work with test data', async ({ page, createTmpDir }) => { + const testDir = await createTmpDir('test-data'); + + // Create test files + await fs.writeFile(path.join(testDir, 'test.bru'), testContent); + + // Use in test + await page.getByLabel('Open Collection').click(); + await page.getByText(testDir).click(); +}); +``` + +## Examples + +### Example 1: Basic Collection Creation + +```typescript +import { test, expect } from '../../playwright'; + +test('should create a new collection', async ({ page, createTmpDir }) => { + const testDir = await createTmpDir('new-collection'); + + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').fill('My Test Collection'); + await page.getByLabel('Location').fill(testDir); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('My Test Collection')).toBeVisible(); +}); +``` + +### Example 2: Request Creation and Execution + +```typescript +import { test, expect } from '../../playwright'; + +test('should create and execute HTTP request', async ({ page, createTmpDir }) => { + const testDir = await createTmpDir('request-test'); + + // Create collection + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').fill('Request Test'); + await page.getByLabel('Location').fill(testDir); + await page.getByRole('button', { name: 'Create' }).click(); + + // Create request + await page.locator('#create-new-tab').getByRole('img').click(); + await page.getByPlaceholder('Request Name').fill('Test Request'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('http://localhost:8081/ping'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Execute request + await page.locator('#send-request').getByRole('img').nth(2).click(); + + // Verify response + await expect(page.getByRole('main')).toContainText('200 OK'); +}); +``` + +### Example 3: Environment Management + +```typescript +import { test, expect } from '../../playwright'; + +test('should create and use environment variables', async ({ page, createTmpDir }) => { + const testDir = await createTmpDir('env-test'); + + // Setup collection + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').fill('Environment Test'); + await page.getByLabel('Location').fill(testDir); + await page.getByRole('button', { name: 'Create' }).click(); + + // Create environment + await page.getByRole('button', { name: 'Environments' }).click(); + await page.getByRole('button', { name: 'Add Environment' }).click(); + await page.getByLabel('Environment Name').fill('Development'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Add variable + await page.getByRole('button', { name: 'Add Variable' }).click(); + await page.getByLabel('Variable Name').fill('API_URL'); + await page.getByLabel('Variable Value').fill('http://localhost:3000'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('API_URL')).toBeVisible(); +}); +``` + +## Troubleshooting + +### Common Issues + +1. **Electron App Not Starting** + + ```bash + # Ensure dependencies are installed + npm install + + # Try running the app manually first + npm run dev:electron + ``` + +2. **Tests Timing Out** + + ```typescript + // Increase timeout for specific test + test('slow test', async ({ page }) => { + test.setTimeout(60000); // 60 seconds + // Test steps + }); + ``` + +3. **Element Not Found** + + ```typescript + // Wait for element to be present + await page.waitForSelector('[data-testid="element"]'); + + // Or use more specific selectors + await page.getByRole('button', { name: 'Exact Button Text' }).click(); + ``` + +4. **Flaky Tests** + + ```typescript + // Use stable selectors + await page.getByTestId('stable-id').click(); + + // Wait for state changes + await page.waitForLoadState('networkidle'); + ``` + +### Debug Mode + +```bash +# Run with debug mode +npx playwright test --debug + +# Run specific test in debug mode +npx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts +``` + +### Trace Analysis + +```bash +# Run with trace recording +npx playwright test --trace on + +# View trace in browser +npx playwright show-trace test-results/trace-*.zip +``` + +## Configuration + +The Playwright configuration is in `playwright.config.ts`: + +```typescript +export default defineConfig({ + testDir: './e2e-tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? undefined : 1, + + projects: [ + { + name: 'Bruno Electron App' + } + ], + + webServer: [ + { + command: 'npm run dev:web', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI + }, + { + command: 'npm start --workspace=packages/bruno-tests', + url: 'http://localhost:8081/ping', + reuseExistingServer: !process.env.CI + } + ] +}); +``` + +## Additional Resources + +- [Playwright Documentation](https://playwright.dev/) +- [Playwright Test API](https://playwright.dev/docs/api/class-test) +- [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication) +- [Bruno Project Structure](../readme.md) + +--- + +For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository. From cd80332de95f4b4a14ff4c83d2e0eeea37d9e4d9 Mon Sep 17 00:00:00 2001 From: bernborgess Date: Wed, 3 Sep 2025 12:46:05 -0300 Subject: [PATCH 03/18] fix: Make globalShortcut only active when app is focused --- packages/bruno-electron/src/index.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 364860bd5..e9026d83c 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -169,12 +169,6 @@ app.on('ready', async () => { return { action: 'deny' }; }); - // Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996 - globalShortcut.register('Ctrl+=', () => { - mainWindow.webContents.setZoomLevel(mainWindow.webContents.getZoomLevel() + 1); - }); - - mainWindow.webContents.on('did-finish-load', async () => { let ogSend = mainWindow.webContents.send; @@ -218,3 +212,17 @@ app.on('window-all-closed', app.quit); app.on('open-file', (event, path) => { openCollection(mainWindow, collectionWatcher, path); }); + + +// Register the global shortcuts +app.on('browser-window-focus', () => { + // Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996 + globalShortcut.register('Ctrl+=', () => { + mainWindow.webContents.setZoomLevel(mainWindow.webContents.getZoomLevel() + 1); + }); +}) + +// Disable global shortcuts when not focused +app.on('browser-window-blur', () => { + globalShortcut.unregisterAll() +}) \ No newline at end of file From db35e7059c0573d7877a43485e2408df6cb0a5e6 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Sat, 6 Sep 2025 17:17:51 +0530 Subject: [PATCH 04/18] Merge pull request #5438 from Pragadesh-45/feat/multiline-values-for-env-vars Feat/ Add Multiline Support for Enviroment Variables --- .../multiline-variables/collection/bruno.json | 5 + .../collection/collection.bru | 5 + .../collection/environments/Test.bru | 8 ++ .../collection/multiline-test.bru | 44 +++++++ .../collection/request.bru | 38 ++++++ .../init-user-data/collection-security.json | 10 ++ .../init-user-data/preferences.json | 28 +++++ .../read-multiline-environment.spec.ts | 35 ++++++ .../write-multiline-variable.spec.ts | 94 ++++++++++++++ .../Environments/EnvironmentSelector/index.js | 4 +- .../EnvironmentVariables/index.js | 5 +- .../EnvironmentVariables/index.js | 4 +- .../ResponsePane/StatusCode/index.js | 2 +- .../Sidebar/Collections/Collection/index.js | 2 +- packages/bruno-lang/v2/src/envToJson.js | 43 ++++++- packages/bruno-lang/v2/src/jsonToBru.js | 19 +-- packages/bruno-lang/v2/src/jsonToEnv.js | 6 +- packages/bruno-lang/v2/src/utils.js | 35 +++++- .../bruno-lang/v2/tests/envToJson.spec.js | 112 +++++++++++++++++ .../bruno-lang/v2/tests/jsonToEnv.spec.js | 117 ++++++++++++++++-- packages/bruno-lang/v2/tests/utils.spec.js | 21 ++++ 21 files changed, 596 insertions(+), 41 deletions(-) create mode 100644 e2e-tests/environments/multiline-variables/collection/bruno.json create mode 100644 e2e-tests/environments/multiline-variables/collection/collection.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/environments/Test.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/multiline-test.bru create mode 100644 e2e-tests/environments/multiline-variables/collection/request.bru create mode 100644 e2e-tests/environments/multiline-variables/init-user-data/collection-security.json create mode 100644 e2e-tests/environments/multiline-variables/init-user-data/preferences.json create mode 100644 e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts create mode 100644 e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts create mode 100644 packages/bruno-lang/v2/tests/utils.spec.js diff --git a/e2e-tests/environments/multiline-variables/collection/bruno.json b/e2e-tests/environments/multiline-variables/collection/bruno.json new file mode 100644 index 000000000..b2f6c48cd --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "multiline-variables", + "type": "collection" +} diff --git a/e2e-tests/environments/multiline-variables/collection/collection.bru b/e2e-tests/environments/multiline-variables/collection/collection.bru new file mode 100644 index 000000000..66d9f88e4 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/collection.bru @@ -0,0 +1,5 @@ +meta { + name: multiline-variables + type: collection + version: 1.0.0 +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/collection/environments/Test.bru b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru new file mode 100644 index 000000000..7a9190cd1 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/environments/Test.bru @@ -0,0 +1,8 @@ +vars { + host: https://www.httpfaker.org + multiline_data: ''' + line1 + line2 + line3 + ''' +} diff --git a/e2e-tests/environments/multiline-variables/collection/multiline-test.bru b/e2e-tests/environments/multiline-variables/collection/multiline-test.bru new file mode 100644 index 000000000..4f19735c9 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/multiline-test.bru @@ -0,0 +1,44 @@ +meta { + name: multiline-test + type: http + seq: 2 +} + +post { + url: {{host}}/api/echo + body: json + auth: none +} + +body:json { + {{multiline_data_json}} +} + +tests { + test("should post multiline data successfully", function() { + expect(res.getStatus()).to.equal(200); + }); + + test("should resolve multiline_data_json variable correctly", function() { + const body = res.getBody(); + // HTTP Faker echo endpoint returns the request body in body.body + // Verify the multiline JSON variable was resolved and parsed correctly + expect(body.body.user.name).to.equal("John Doe"); + expect(body.body.user.email).to.equal("john@example.com"); + expect(body.body.user.preferences.theme).to.equal("dark"); + expect(body.body.user.preferences.notifications).to.equal(true); + }); + + test("should preserve JSON structure from multiline variable", function() { + const body = res.getBody(); + // Verify the complete JSON structure was preserved + expect(body.body.metadata.created).to.equal("2025-09-03"); + expect(body.body.metadata.version).to.equal("1.0"); + }); + + test("should resolve host variable in URL", function() { + const body = res.getBody(); + // Verify the host variable was resolved in the request URL + expect(body.url).to.equal("https://www.httpfaker.org/api/echo"); + }); +} diff --git a/e2e-tests/environments/multiline-variables/collection/request.bru b/e2e-tests/environments/multiline-variables/collection/request.bru new file mode 100644 index 000000000..1f82b0950 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/collection/request.bru @@ -0,0 +1,38 @@ +meta { + name: request + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo + body: text + auth: none +} + +body:json { + Ping Test Request + Host: {{host}} + + Multiline Data: + {{multiline_data}} + + End of multiline content. +} + +body:text { + {{host}} + {{multiline_data}} +} + +tests { + test("should get 200 response", function() { + expect(res.getStatus()).to.equal(200); + }); + + test("should resolve multiline_data variable correctly", function() { + const body = res.getBody(); + // Verify the multiline variable was resolved and contains all three lines + expect(body.body).to.equal("https://www.httpfaker.org\nline1\nline2\nline3"); + }); +} diff --git a/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json b/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json new file mode 100644 index 000000000..5f51bd286 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/e2e-tests/environments/multiline-variables/collection", + "securityConfig": { + "jsSandboxMode": "developer" + } + } + ] +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/init-user-data/preferences.json b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json new file mode 100644 index 000000000..9374c16fd --- /dev/null +++ b/e2e-tests/environments/multiline-variables/init-user-data/preferences.json @@ -0,0 +1,28 @@ +{ + "maximized": true, + "lastOpenedCollections": [ + "{{projectRoot}}/e2e-tests/environments/multiline-variables/collection" + ], + "request": { + "sslVerification": false, + "customCaCertificate": { + "enabled": false, + "filePath": null + } + }, + "font": { + "codeFont": "default" + }, + "proxy": { + "enabled": false, + "protocol": "http", + "hostname": "", + "port": "", + "auth": { + "enabled": false, + "username": "", + "password": "" + }, + "bypassProxy": "" + } +} \ No newline at end of file diff --git a/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts new file mode 100644 index 000000000..0e0856816 --- /dev/null +++ b/e2e-tests/environments/multiline-variables/read-multiline-environment.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Multiline Variables - Read Environment Test', () => { + test('should read existing multiline environment variables', async ({ pageWithUserData: page }) => { + test.setTimeout(30 * 1000); + + // open the collection + await expect(page.getByTitle('multiline-variables')).toBeVisible(); + await page.getByTitle('multiline-variables').click(); + + // open request + await expect(page.getByTitle('request', { exact: true })).toBeVisible(); + await page.getByTitle('request', { exact: true }).click(); + + // open environment dropdown + await expect(page.getByTitle('No Environment')).toBeVisible(); + await page.getByTitle('No Environment').click(); + + // select test environment + await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); + + // send request + const sendButton = page.locator('#send-request').getByRole('img').nth(2); + await expect(sendButton).toBeVisible(); + await sendButton.click(); + await expect(page.locator('.response-status-code.text-ok')).toBeVisible(); + await expect(page.locator('.response-status-code')).toContainText('200'); + + // response pane should contain the expected multiline text in JSON body + const responsePane = page.locator('.response-pane'); + await expect(responsePane).toContainText('"body": "https://www.httpfaker.org\\nline1\\nline2\\nline3"'); + }); +}); diff --git a/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts b/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts new file mode 100644 index 000000000..f2aff96fe --- /dev/null +++ b/e2e-tests/environments/multiline-variables/write-multiline-variable.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Multiline Variables - Write Test', () => { + test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => { + test.setTimeout(60 * 1000); + + // open the collection + await expect(page.getByTitle('multiline-variables')).toBeVisible(); + await page.getByTitle('multiline-variables').click(); + + // open request + await expect(page.getByTitle('multiline-test', { exact: true })).toBeVisible(); + await page.getByTitle('multiline-test', { exact: true }).click(); + + // open environment dropdown + await expect(page.getByTitle('No Environment')).toBeVisible(); + await page.getByTitle('No Environment').click(); + + // select test environment + await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); + await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); + + // select configure button from environment dropdown + await expect(page.getByTitle('Test', { exact: true })).toBeVisible(); + await page.getByTitle('Test', { exact: true }).click(); + + // open environment configuration + await expect(page.locator('#Configure')).toBeVisible(); + await page.locator('#Configure').click(); + + // add variable + await page.getByRole('button', { name: /Add.*Variable/i }).click(); + const valueTextarea = page.locator('.bruno-modal-card textarea').last(); + await expect(valueTextarea).toBeVisible(); + + + const jsonValue = `{ + "user": { + "name": "John Doe", + "email": "john@example.com", + "preferences": { + "theme": "dark", + "notifications": true + } + }, + "metadata": { + "created": "2025-09-03", + "version": "1.0" + } +}`; + + // fill variable value + await valueTextarea.fill(jsonValue); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.type('multiline_data_json'); + + // save variable and close config + const saveVarButton = page.getByRole('button', { name: /Save/i }); + await expect(saveVarButton).toBeVisible(); + await saveVarButton.click(); + + await expect(page.locator('.close.cursor-pointer')).toBeVisible(); + await page.locator('.close.cursor-pointer').click(); + + // send request + const sendButton = page.locator('#send-request').getByRole('img').nth(2); + await expect(sendButton).toBeVisible(); + await sendButton.click(); + + // wait for response status + await expect(page.locator('.response-status-code.text-ok')).toBeVisible(); + await expect(page.locator('.response-status-code')).toContainText('200'); + + // verify multiline JSON variable resolution in response + const expectedBody = + '{\n "user": {\n "name": "John Doe",\n "email": "john@example.com",\n "preferences": {\n "theme": "dark",\n "notifications": true\n }\n },\n "metadata": {\n "created": "2025-09-03",\n "version": "1.0"\n }\n}'; + await expect(page.locator('.response-pane')).toContainText(`"body": ${JSON.stringify(expectedBody)}`); + }); + + // clean up created variable after test + test.afterEach(async () => { + const fs = require('fs'); + const path = require('path'); + + const testBruPath = path.join(__dirname, 'collection/environments/Test.bru'); + let content = fs.readFileSync(testBruPath, 'utf8'); + + // remove the multiline_data_json variable and its content + content = content.replace(/\s*multiline_data_json:\s*'''\s*[\s\S]*?\s*'''/g, ''); + + fs.writeFileSync(testBruPath, content); + }); +}); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index 848048c13..a8246237c 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => { const Icon = forwardRef((props, ref) => { return (
-

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

+

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

); @@ -82,7 +82,7 @@ const EnvironmentSelector = ({ collection }) => { handleSettingsIconClick(); dropdownTippyRef.current.hide(); }}> -
+
Configure diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 5ba3b0797..d02cc494a 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -5,7 +5,7 @@ import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCh import { useTheme } from 'providers/Theme'; import { useDispatch, useSelector } from 'react-redux'; import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; -import SingleLineEditor from 'components/SingleLineEditor'; +import MultiLineEditor from 'components/MultiLineEditor'; import StyledWrapper from './StyledWrapper'; import { uuid } from 'utils/common'; import { useFormik } from 'formik'; @@ -214,7 +214,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
- + Add Variable diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 10ab9fba3..f539543dd 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { IconTrash, IconAlertCircle } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; -import SingleLineEditor from 'components/SingleLineEditor'; +import MultiLineEditor from 'components/MultiLineEditor'; import StyledWrapper from './StyledWrapper'; import { uuid } from 'utils/common'; import { useFormik } from 'formik'; @@ -147,7 +147,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
- { }; return ( - + {status} {statusCodePhraseMap[status]} ); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 0f44b467a..67ffcb68b 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -232,7 +232,7 @@ const Collection = ({ collection, searchText }) => { onClick={handleCollectionCollapse} onDoubleClick={handleCollectionDoubleClick} /> -