Merge branch 'main' into feat/websocket-engine

This commit is contained in:
Siddharth Gelera
2025-09-15 10:53:09 +05:30
172 changed files with 4482 additions and 433 deletions

View File

@@ -0,0 +1,26 @@
name: 'Setup Node Dependencies'
description: 'Install Node.js and npm dependencies'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: v22.17.0
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install node dependencies
shell: bash
run: npm ci --legacy-peer-deps
- name: Build libraries
shell: bash
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:bruno-filestore

View File

@@ -0,0 +1,36 @@
name: 'Run Basic SSL CLI Tests - Linux'
description: 'Run basic SSL CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to basic SSL test collection directory
cd tests/ssl/basic-ssl/collections/badssl
echo "basic ssl success"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "with default/system ca certs"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
# navigate to self-signed SSL test collection directory
cd ../self-signed-badssl
echo "self-signed ssl with validation disabled"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
echo "self-signed ssl with default/system ca certs"
echo "request will error"
# should fail
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -0,0 +1,33 @@
name: 'Run Custom CA Certs CLI Tests - Linux'
description: 'Run custom CA certs CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to CA certificates test collection directory
cd tests/ssl/custom-ca-certs/collection
echo "custom valid ca cert"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "custom valid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert"
echo "request will error"
# should fail
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -0,0 +1,19 @@
name: 'Run SSL E2E Tests - Linux'
description: 'Run SSL E2E tests on Linux'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: bash
run: |
set -euo pipefail
xvfb-run npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-linux
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,26 @@
name: 'Setup CA Certificates - Linux'
description: 'Setup CA certificates and start test server for custom CA certs tests on Linux'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "starting server in background"
node index.js &
echo "server started with PID: $!"

View File

@@ -0,0 +1,15 @@
name: 'Setup Custom CA Certs Feature Dependencies - Linux'
description: 'Setup feature-specific dependencies for custom CA certs tests on Linux'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for custom CA certs
shell: bash
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb libxml2-utils
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox

View File

@@ -0,0 +1,36 @@
name: 'Run Basic SSL CLI Tests - macOS'
description: 'Run basic SSL CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to basic SSL test collection directory
cd tests/ssl/basic-ssl/collections/badssl
echo "basic ssl success"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "with default/system ca certs"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
# navigate to self-signed SSL test collection directory
cd ../self-signed-badssl
echo "self-signed ssl with validation disabled"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
echo "self-signed ssl with default/system ca certs"
echo "request will error"
# should fail
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -0,0 +1,33 @@
name: 'Run Custom CA Certs CLI Tests - macOS'
description: 'Run custom CA certs CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to CA certificates test collection directory
cd tests/ssl/custom-ca-certs/collection
echo "custom valid ca cert"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "custom valid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert"
echo "request will error"
# should fail
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -0,0 +1,17 @@
name: 'Run SSL E2E Tests - macOS'
description: 'Run SSL E2E tests on macOS'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: bash
run: |
npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-macos
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,26 @@
name: 'Setup CA Certificates - macOS'
description: 'Setup CA certificates and start test server for custom CA certs tests on macOS'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "starting server in background"
node index.js &
echo "server started with PID: $!"

View File

@@ -0,0 +1,9 @@
name: 'Setup Custom CA Certs Feature Dependencies - macOS'
description: 'Setup feature-specific dependencies for custom CA certs tests on macOS'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for custom CA certs
shell: bash
run: |
brew install libxml2

View File

@@ -0,0 +1,50 @@
name: 'Run Basic SSL CLI Tests - Windows'
description: 'Run basic SSL CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# navigate to basic SSL test collection directory
Set-Location tests\ssl\basic-ssl\collections\badssl
Write-Host "basic ssl success"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml1 = Get-Content junit1.xml
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount1 -ne 1) { exit 1 }
Write-Host "with default/system ca certs"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml2 = Get-Content junit2.xml
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount2 -ne 1) { exit 1 }
# navigate to self-signed SSL test collection directory
Set-Location ..\self-signed-badssl
Write-Host "self-signed ssl with validation disabled"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml3 = Get-Content junit3.xml
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount3 -ne 1) { exit 1 }
Write-Host "self-signed ssl with default/system ca certs"
Write-Host "request will error"
# should fail
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
# Ignore the exit code - we expect this to fail
[xml]$xml4 = Get-Content junit4.xml
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
if ($errorCount4 -ne 1) { exit 1 }

View File

@@ -0,0 +1,47 @@
name: 'Run Custom CA Certs CLI Tests - Windows'
description: 'Run custom CA certs CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# navigate to CA certificates test collection directory
Set-Location tests\ssl\custom-ca-certs\collection
Write-Host "custom valid ca cert"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --cacert ..\server\certs\ca-cert.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml1 = Get-Content junit1.xml
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount1 -ne 1) { exit 1 }
Write-Host "custom valid ca cert with defaults"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --cacert ..\server\certs\ca-cert.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml2 = Get-Content junit2.xml
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount2 -ne 1) { exit 1 }
Write-Host "custom invalid ca cert"
Write-Host "request will error"
# should fail
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --cacert ..\server\certs\ca-key.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
# Ignore the exit code - we expect this to fail
[xml]$xml3 = Get-Content junit3.xml
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
if ($errorCount3 -ne 1) { exit 1 }
Write-Host "custom invalid ca cert with defaults"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --cacert ..\server\certs\ca-key.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml4 = Get-Content junit4.xml
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount4 -ne 1) { exit 1 }

View File

@@ -0,0 +1,17 @@
name: 'Run SSL E2E Tests - Windows'
description: 'Run SSL E2E tests on Windows'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: pwsh
run: |
npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-windows
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,25 @@
name: 'Setup CA Certificates - Windows'
description: 'Setup CA certificates and start test server for custom CA certs tests on Windows'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Set-Location tests\ssl\custom-ca-certs\server
Write-Host "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: pwsh
run: |
Set-StrictMode -Version Latest
Set-Location tests\ssl\custom-ca-certs\server
Write-Host "starting server in background"
Start-Process -FilePath "node" -ArgumentList "index.js" -PassThru -WindowStyle Hidden

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'

91
.github/workflows/ssl-tests.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: SSL Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
tests-for-linux:
name: SSL Tests - Linux
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/linux/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
tests-for-macos:
name: SSL Tests - macOS
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/macos/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
tests-for-windows:
name: SSL Tests - Windows
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/windows/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests

View File

@@ -14,7 +14,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -65,7 +65,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -107,7 +107,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: v22.11.x
- name: Install dependencies

View File

@@ -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.

View File

@@ -1,33 +0,0 @@
import { test, expect } from '../../playwright';
test.describe.serial('Persistent Environment Test', () => {
test.setTimeout(2 * 10 * 1000);
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
await page.locator('#sidebar-collection-name').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByText('ping', { exact: true }).click();
await page.getByText('No Environment').click();
await page.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
await page.locator('#send-request').getByRole('img').nth(2).click();
await page.waitForTimeout(1000);
await page.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
await page.getByText('×').click();
const newApp = await restartApp();
const newPage = await newApp.firstWindow();
await newPage.locator('#sidebar-collection-name').click();
await newPage.getByRole('button', { name: 'Save' }).click();
await newPage.getByText('ping', { exact: true }).click();
await newPage.getByText('No Environment').click();
await newPage.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
await newPage.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
await newPage.getByText('Configure', { exact: true }).click();
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
await newPage.getByText('×').click();
await newPage.waitForTimeout(1000);
await newPage.close();
});
});

View File

@@ -1,40 +0,0 @@
import { test, expect } from '../../playwright';
test.describe.serial('Persistent Environment Test', () => {
test.setTimeout(2 * 10 * 1000);
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
await page.locator('#sidebar-collection-name').click();
await page.getByText('ping2', { exact: true }).click();
await page.getByText('Env', { exact: true }).click();
await page.getByText('Stage', { exact: true }).click();
await page.locator('#send-request').getByRole('img').nth(2).click();
await page.waitForTimeout(1000);
await page
.locator('div')
.filter({ hasText: /^Stage$/ })
.nth(3)
.click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
await page.getByText('×').click();
const newApp = await restartApp();
const newPage = await newApp.firstWindow();
await newPage.locator('#sidebar-collection-name').click();
await newPage.getByRole('button', { name: 'Save' }).click();
await newPage.getByText('ping2', { exact: true }).click();
await newPage.getByText('No Environment').click();
await newPage.getByText('Stage').click();
await newPage
.locator('div')
.filter({ hasText: /^Stage$/ })
.nth(3)
.click();
await newPage.getByText('Configure', { exact: true }).click();
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).not.toBeVisible();
await newPage.getByText('×').click();
await newPage.waitForTimeout(1000);
await newPage.close();
});
});

View File

@@ -1,4 +0,0 @@
vars {
host: https://testbench-sanity.usebruno.com
persistent-env-test: persistent-env-test-value
}

View File

@@ -1,6 +0,0 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
]
}

337
package-lock.json generated
View File

@@ -1605,7 +1605,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz",
"integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -1623,7 +1623,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz",
"integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
@@ -1640,7 +1640,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1658,7 +1658,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@babel/helper-member-expression-to-functions": {
@@ -1729,7 +1729,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz",
"integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -1804,7 +1804,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz",
"integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -1847,7 +1847,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz",
"integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -1864,7 +1864,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz",
"integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -1880,7 +1880,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -1896,7 +1896,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz",
"integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -1914,7 +1914,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz",
"integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -1949,7 +1949,7 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2048,7 +2048,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz",
"integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2064,7 +2064,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
"integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2246,7 +2246,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -2263,7 +2263,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz",
"integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2279,7 +2279,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz",
"integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -2297,7 +2297,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
"integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -2315,7 +2315,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz",
"integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2331,7 +2331,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz",
"integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2363,7 +2363,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz",
"integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.25.9",
@@ -2380,7 +2380,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz",
"integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -2401,7 +2401,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -2411,7 +2411,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
"integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -2428,7 +2428,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz",
"integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2444,7 +2444,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz",
"integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -2461,7 +2461,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz",
"integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2477,7 +2477,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -2494,7 +2494,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2510,7 +2510,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz",
"integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2526,7 +2526,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz",
"integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2558,7 +2558,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -2575,7 +2575,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz",
"integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -2593,7 +2593,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz",
"integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2609,7 +2609,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz",
"integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2625,7 +2625,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz",
"integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2641,7 +2641,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz",
"integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2657,7 +2657,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz",
"integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -2690,7 +2690,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz",
"integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -2709,7 +2709,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz",
"integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -2726,7 +2726,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -2743,7 +2743,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz",
"integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2774,7 +2774,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
"integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2790,7 +2790,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
"integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -2808,7 +2808,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
"integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -2825,7 +2825,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
"integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2857,7 +2857,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz",
"integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2889,7 +2889,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
"integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -2907,7 +2907,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz",
"integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -2992,7 +2992,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz",
"integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -3009,7 +3009,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz",
"integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -3026,7 +3026,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz",
"integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3042,7 +3042,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz",
"integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3058,7 +3058,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz",
"integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -3075,7 +3075,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz",
"integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3091,7 +3091,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz",
"integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3107,7 +3107,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz",
"integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3142,7 +3142,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz",
"integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -3158,7 +3158,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz",
"integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -3175,7 +3175,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz",
"integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -3192,7 +3192,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz",
"integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -3209,7 +3209,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz",
"integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.26.0",
@@ -3310,7 +3310,7 @@
"version": "0.1.6-no-external-plugins",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -7904,8 +7904,8 @@
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -7924,8 +7924,8 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -7937,8 +7937,8 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -7952,8 +7952,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT",
"peer": true
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
@@ -8033,6 +8033,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -8287,6 +8288,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -8318,6 +8320,7 @@
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -8328,6 +8331,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -9381,6 +9385,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
@@ -9748,7 +9753,7 @@
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz",
"integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.6",
@@ -9763,7 +9768,7 @@
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.2",
@@ -9777,7 +9782,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz",
"integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3"
@@ -9937,6 +9942,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -11527,7 +11542,7 @@
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.2"
@@ -12577,6 +12592,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-converter": {
@@ -13568,7 +13584,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"devOptional": true,
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -14283,6 +14299,13 @@
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT",
"optional": true
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -16148,7 +16171,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -18399,7 +18422,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/lodash.flow": {
@@ -18537,11 +18560,34 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/macos-export-certificate-and-key": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.2.4.tgz",
"integrity": "sha512-y5QZEywlBNKd+EhPZ1Hz1FmDbbeQKtuVHJaTlawdl7vXw9bi/4tJB2xSMwX4sMVcddy3gbQ8K0IqXAi2TpDo2g==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^4.3.0"
}
},
"node_modules/macos-export-certificate-and-key/node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
"license": "MIT",
"optional": true
},
"node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
@@ -19965,7 +20011,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -22305,14 +22351,14 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
"integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -22331,7 +22377,7 @@
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
"integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -22341,7 +22387,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
"integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
@@ -22359,14 +22405,14 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
"integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
"devOptional": true,
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.0.2"
@@ -22379,7 +22425,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -22535,7 +22581,7 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
@@ -24747,7 +24793,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -24827,6 +24873,16 @@
"jscat": "bundle.js"
}
},
"node_modules/system-ca": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz",
"integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==",
"license": "Apache-2.0",
"optionalDependencies": {
"macos-export-certificate-and-key": "^1.2.0",
"win-export-certificate-and-key": "^2.1.0"
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
@@ -25614,7 +25670,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -25675,7 +25731,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
"integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -25685,7 +25741,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
"integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^2.0.0",
@@ -25699,7 +25755,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
"integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -25709,7 +25765,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -26303,6 +26359,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/win-export-certificate-and-key": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-2.1.0.tgz",
"integrity": "sha512-WeMLa/2uNZcS/HWGKU2G1Gzeh3vHpV/UFvwLhJLKxPHYFAbubxxVcJbqmPXaqySWK1Ymymh16zKK5WYIJ3zgzA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^3.1.0"
}
},
"node_modules/win-export-certificate-and-key/node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"license": "MIT",
"optional": true
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -26676,7 +26754,7 @@
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"autoprefixer": "10.4.20",
@@ -28006,26 +28084,6 @@
"semver": "bin/semver.js"
}
},
"packages/bruno-app/node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"packages/bruno-app/node_modules/@testing-library/react": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
@@ -28122,23 +28180,6 @@
],
"license": "CC-BY-4.0"
},
"packages/bruno-app/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"packages/bruno-app/node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
@@ -28240,41 +28281,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-app/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"packages/bruno-app/node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"packages/bruno-app/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"packages/bruno-app/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -31923,8 +31929,9 @@
"axios": "^1.9.0",
"grpc-reflection-js": "^0.3.0",
"is-ip": "^5.0.1",
"tough-cookie": "^6.0.0",
"ws": "^8.18.3"
"ws": "^8.18.3",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.22.0",

View File

@@ -65,7 +65,8 @@
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test",
"test:e2e": "playwright test --project=default",
"test:e2e:ssl": "playwright test --project=ssl",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
},

View File

@@ -93,7 +93,7 @@
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"autoprefixer": "10.4.20",

View File

@@ -18,8 +18,8 @@ const EnvironmentSelector = ({ collection }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<div ref={ref} className="current-environment collection-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
<p className="text-nowrap truncate max-w-32" title={activeEnvironment ? activeEnvironment.name : 'No Environment'}>{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);
@@ -82,7 +82,7 @@ const EnvironmentSelector = ({ collection }) => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<div className="pr-2 text-gray-600" id="Configure">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Configure</span>

View File

@@ -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';
@@ -173,7 +173,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
return (
<StyledWrapper className="w-full mt-6 mb-6">
<div className="h-[50vh] overflow-y-auto w-full">
<table>
<table className="environment-variables">
<thead>
<tr>
<td className="text-center">Enabled</td>
@@ -214,7 +214,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</td>
<td className="flex flex-row flex-nowrap items-center">
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
<MultiLineEditor
theme={storedTheme}
collection={_collection}
name={`${index}.value`}
@@ -253,6 +253,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
ref={addButtonRef}
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
onClick={addVariable}
id="add-variable"
>
+ Add Variable
</button>

View File

@@ -19,7 +19,7 @@ const EnvironmentSelector = () => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className={`current-environment flex flex-row gap-1 rounded-xl text-xs cursor-pointer items-center justify-center select-none ${activeGlobalEnvironmentUid? 'environment-active': ''}`}>
<div ref={ref} className={`current-environment global-environment flex flex-row gap-1 rounded-xl text-xs cursor-pointer items-center justify-center select-none ${activeGlobalEnvironmentUid? 'environment-active': ''}`}>
<ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId" className='flex flex-row'>
<IconWorld className="globe" size={16} strokeWidth={1.5} />
{

View File

@@ -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
</td>
<td className="flex flex-row flex-nowrap">
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
<MultiLineEditor
theme={storedTheme}
name={`${index}.value`}
value={variable.value}

View File

@@ -19,6 +19,11 @@ const BETA_FEATURES = [
id: 'websocket',
label: 'Web Socket Support',
description: 'Enable Web Socket request support for making realtime calls to services'
},
{
id: 'nodevm',
label: 'Node VM Runtime',
description: 'Enable Node VM runtime for JavaScript execution in Developer Mode'
}
];
@@ -73,7 +78,7 @@ const Beta = ({ close }) => {
.catch((err) => console.log(err) && toast.error('Failed to update beta preferences'));
};
const hasAnyBetaFeatures = Object.values(formik.values).length > 0;
const hasAnyBetaFeatures = BETA_FEATURES.length > 0;
return (
<StyledWrapper>

View File

@@ -176,11 +176,11 @@ const General = ({ close }) => {
name="keepDefaultCaCertificates.enabled"
checked={formik.values.keepDefaultCaCertificates.enabled}
onChange={formik.handleChange}
className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
disabled={formik.values.customCaCertificate.enabled ? false : true}
className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}
disabled={formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? false : true}
/>
<label
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}
htmlFor="keepDefaultCaCertificatesEnabled"
>
Keep Default CA Certificates

View File

@@ -112,6 +112,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div
title="Generate Code"
className="infotip mr-3"
onClick={(e) => {
handleGenerateCode(e);
@@ -128,6 +129,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
</span>
</div>
<div
title="Save Request"
className="infotip mr-3"
onClick={(e) => {
e.stopPropagation();

View File

@@ -16,7 +16,7 @@ const StatusCode = ({ status }) => {
};
return (
<StyledWrapper className={getTabClassname(status)}>
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`}>
{status} {statusCodePhraseMap[status]}
</StyledWrapper>
);

View File

@@ -60,8 +60,8 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'
const [{ isDragging }, drag, dragPreview] = useDrag({
type: `collection-item-${collectionUid}`,
item,
type: 'collection-item',
item: { ...item, sourceCollectionUid: collectionUid },
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
@@ -92,10 +92,15 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => {
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
const { uid: draggedItemUid, pathname: draggedItemPathname, sourceCollectionUid } = draggedItem;
if (draggedItemUid === targetItemUid) return false;
// For cross-collection moves, we allow the drop
if (sourceCollectionUid !== collectionUid) {
return true;
}
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname });
if (!newPathname) return false;
@@ -105,7 +110,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
};
const [{ isOver, canDrop }, drop] = useDrop({
accept: `collection-item-${collectionUid}`,
accept: 'collection-item',
hover: (draggedItem, monitor) => {
const { uid: targetItemUid } = item;
const { uid: draggedItemUid } = draggedItem;

View File

@@ -7,6 +7,7 @@ const Wrapper = styled.div`
user-select: none;
padding-left: 8px;
font-weight: 600;
border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent;
.rotate-90 {
transform: rotateZ(90deg);
@@ -66,6 +67,7 @@ const Wrapper = styled.div`
}
&.drop-target {
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
transition: ${(props) => props.theme.dragAndDrop.transition};
}
@@ -95,15 +97,6 @@ const Wrapper = styled.div`
}
}
.collection-name.drop-target {
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
border-radius: 4px;
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
margin: -2px;
transition: ${(props) => props.theme.dragAndDrop.transition};
box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg};
}
#sidebar-collection-name {
white-space: nowrap;
text-overflow: ellipsis;

View File

@@ -34,6 +34,7 @@ const Collection = ({ collection, searchText }) => {
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [dropType, setDropType] = useState(null);
const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const collectionRef = useRef(null);
@@ -42,7 +43,7 @@ const Collection = ({ collection, searchText }) => {
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
const MenuIcon = forwardRef((props, ref) => {
const MenuIcon = forwardRef((_props, ref) => {
return (
<div ref={ref} className="pr-2">
<IconDots size={22} />
@@ -101,7 +102,7 @@ const Collection = ({ collection, searchText }) => {
}
};
const handleDoubleClick = (event) => {
const handleDoubleClick = (_event) => {
dispatch(makeTabPermanent({ uid: collection.uid }))
};
@@ -118,7 +119,7 @@ const Collection = ({ collection, searchText }) => {
e.preventDefault();
};
const handleRightClick = (event) => {
const handleRightClick = (_event) => {
const _menuDropdown = menuDropdownTippyRef.current;
if (_menuDropdown) {
let menuDropdownBehavior = 'show';
@@ -140,7 +141,7 @@ const Collection = ({ collection, searchText }) => {
};
const isCollectionItem = (itemType) => {
return itemType.startsWith('collection-item');
return itemType === 'collection-item';
};
const [{ isDragging }, drag, dragPreview] = useDrag({
@@ -155,7 +156,17 @@ const Collection = ({ collection, searchText }) => {
});
const [{ isOver }, drop] = useDrop({
accept: ["collection", `collection-item-${collection.uid}`],
accept: ["collection", "collection-item"],
hover: (_draggedItem, monitor) => {
const itemType = monitor.getItemType();
if (isCollectionItem(itemType)) {
// For collection items, always show full highlight (inside drop)
setDropType('inside');
} else {
// For collections, show line indicator (adjacent drop)
setDropType('adjacent');
}
},
drop: (draggedItem, monitor) => {
const itemType = monitor.getItemType();
if (isCollectionItem(itemType)) {
@@ -163,6 +174,7 @@ const Collection = ({ collection, searchText }) => {
} else {
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
}
setDropType(null);
},
canDrop: (draggedItem) => {
return draggedItem.uid !== collection.uid;
@@ -183,7 +195,8 @@ const Collection = ({ collection, searchText }) => {
}
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
'item-hovered': isOver,
'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)
'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)
'collection-focused-in-tab': isCollectionFocused
});
@@ -232,7 +245,7 @@ const Collection = ({ collection, searchText }) => {
onClick={handleCollectionCollapse}
onDoubleClick={handleCollectionDoubleClick}
/>
<div className="ml-1 w-full" id="sidebar-collection-name">
<div className="ml-1 w-full" id="sidebar-collection-name" title={collection.name}>
{collection.name}
</div>
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
@@ -241,7 +254,7 @@ const Collection = ({ collection, searchText }) => {
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
setShowNewRequestModal(true);
}}
@@ -250,7 +263,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
setShowNewFolderModal(true);
}}
@@ -259,7 +272,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
setShowCloneCollectionModalOpen(true);
}}
@@ -268,7 +281,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
ensureCollectionIsMounted();
handleRun();
@@ -278,7 +291,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
setShowRenameCollectionModal(true);
}}
@@ -287,7 +300,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
ensureCollectionIsMounted();
setShowShareCollectionModal(true);
@@ -297,7 +310,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
setShowRemoveCollectionModal(true);
}}
@@ -306,7 +319,7 @@ const Collection = ({ collection, searchText }) => {
</div>
<div
className="dropdown-item"
onClick={(e) => {
onClick={(_e) => {
menuDropdownTippyRef.current.hide();
viewCollectionSettings();
}}

View File

@@ -733,11 +733,16 @@ export const handleCollectionItemDrop =
(dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
// if its withincollection set the source to current collection,
// if its cross collection set the source to the source collection
const sourceCollectionUid = draggedItem.sourceCollectionUid
const isCrossCollectionMove = sourceCollectionUid && collectionUid !== sourceCollectionUid;
const sourceCollection = isCrossCollectionMove ? findCollectionByUid(state.collections.collections, sourceCollectionUid) : collection;
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection;
const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items);
const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection;
const draggedItemDirectory = findParentItemInCollection(sourceCollection, draggedItemUid) || sourceCollection;
const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items);
const handleMoveToNewLocation = async ({

View File

@@ -6,7 +6,8 @@ import { useSelector } from 'react-redux';
*/
export const BETA_FEATURES = Object.freeze({
GRPC: 'grpc',
WEBSOCKET: 'websocket'
WEBSOCKET: 'websocket',
NODE_VM: 'nodevm'
});
/**

View File

@@ -1,9 +1,7 @@
const os = require('os');
const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
const tls = require('tls');
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
const FormData = require('form-data');
const prepareRequest = require('./prepare-request');
@@ -26,6 +24,7 @@ const { getOAuth2Token } = require('./oauth2');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const { NtlmClient } = require('axios-ntlm');
const { addDigestInterceptor } = require('@usebruno/requests');
const { getCACertificates } = require('@usebruno/requests');
const { encodeUrl } = require('@usebruno/common').utils;
const { sendNetworkRequest } = require('@usebruno/requests');
const { sendGrpcRequest } = require('@usebruno/requests');
@@ -154,22 +153,14 @@ const runSingleRequest = async function (
const insecure = get(options, 'insecure', false);
const noproxy = get(options, 'noproxy', false);
const httpsAgentRequestFields = {};
if (insecure) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
} else {
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
const caCert = caCertArray.find((el) => el);
if (caCert && caCert.length > 1) {
try {
let caCertBuffer = fs.readFileSync(caCert);
if (!options['ignoreTruststore']) {
caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates
}
httpsAgentRequestFields['ca'] = caCertBuffer;
} catch (err) {
console.log('Error reading CA cert file:' + caCert, err);
}
}
const caCertFilePath = options['cacert'];
let caCertificatesData = await getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
let caCertificates = caCertificatesData.caCertificates;
httpsAgentRequestFields['ca'] = caCertificates || [];
}
const interpolationOptions = {

View File

@@ -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()
})

View File

@@ -699,7 +699,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// Recursive function to parse the folder and create files/folders
const parseCollectionItems = (items = [], currentPath) => {
items.forEach(async (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
const content = await stringifyRequestViaWorker(item);
const filePath = path.join(currentPath, item.filename);
safeWriteFileSync(filePath, content);

View File

@@ -1,7 +1,7 @@
const fs = require('fs');
const tls = require('tls');
const fs = require('node:fs');
const path = require('path');
const { get } = require('lodash');
const { getCACertificates } = require('@usebruno/requests');
const { preferencesUtil } = require('../../store/preferences');
const { getBrunoConfig } = require('../../store/bruno-config');
const { interpolateString } = require('./interpolate-string');
@@ -26,16 +26,18 @@ const getCertsAndProxyConfig = async ({
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
if (preferencesUtil.shouldUseCustomCaCertificate()) {
const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath();
if (caCertFilePath) {
let caCertBuffer = fs.readFileSync(caCertFilePath);
if (preferencesUtil.shouldKeepDefaultCaCertificates()) {
caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates
}
httpsAgentRequestFields['ca'] = caCertBuffer;
}
}
let caCertFilePath = preferencesUtil.shouldUseCustomCaCertificate() && preferencesUtil.getCustomCaCertificateFilePath();
let caCertificatesData = await getCACertificates({
caCertFilePath,
shouldKeepDefaultCerts: preferencesUtil.shouldKeepDefaultCaCertificates()
});
let caCertificates = caCertificatesData.caCertificates;
let caCertificatesCount = caCertificatesData.caCertificatesCount;
// configure HTTPS agent with aggregated CA certificates
httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;
httpsAgentRequestFields['ca'] = caCertificates || [];
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {

View File

@@ -70,7 +70,16 @@ const saveCookies = (url, headers) => {
const getJsSandboxRuntime = (collection) => {
const securityConfig = get(collection, 'securityConfig', {});
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
if (securityConfig.jsSandboxMode === 'safe') {
return 'quickjs';
}
if (preferencesUtil.isBetaFeatureEnabled('nodevm')) {
return 'nodevm';
}
return 'vm2';
};
const configureRequest = async (collectionUid, request, envVars, runtimeVariables, processEnvVars, collectionPath) => {

View File

@@ -1,3 +1,4 @@
const path = require('node:path');
const _ = require('lodash');
const Store = require('electron-store');
const { isDirectory } = require('../utils/filesystem');
@@ -12,7 +13,9 @@ class LastOpenedCollections {
}
getAll() {
return this.store.get('lastOpenedCollections') || [];
let collections = this.store.get('lastOpenedCollections') || [];
collections = collections.map(collection => path.resolve(collection));
return collections;
}
add(collectionPath) {

View File

@@ -42,7 +42,8 @@ const defaultPreferences = {
responsePaneOrientation: 'horizontal'
},
beta: {
grpc: false
grpc: false,
nodevm: false
}
};
@@ -80,7 +81,8 @@ const preferencesSchema = Yup.object().shape({
responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical'])
}),
beta: Yup.object({
grpc: Yup.boolean()
grpc: Yup.boolean(),
nodevm: Yup.boolean()
})
});

View File

@@ -331,6 +331,7 @@ const transformRequestToSaveToFilesystem = (item) => {
name: _item.name,
seq: _item.seq,
settings: _item.settings,
tags: _item.tags,
request: {
method: _item.request.method,
url: _item.request.url,

View File

@@ -1,5 +1,5 @@
const parseUrl = require('url').parse;
const https = require('https');
const https = require('node:https');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { interpolateString } = require('../ipc/network/interpolate-string');
const { SocksProxyAgent } = require('socks-proxy-agent');
@@ -87,6 +87,10 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
function createTimelineAgentClass(BaseAgentClass) {
return class extends BaseAgentClass {
constructor(options, timeline) {
let caCertificatesCount = options.caCertificatesCount || {};
delete options.caCertificatesCount;
// For proxy agents, the first argument is the proxy URI and the second is options
if (options?.proxy) {
const { proxy: proxyUri, ...agentOptions } = options;
@@ -118,7 +122,7 @@ function createTimelineAgentClass(BaseAgentClass) {
const tlsOptions = {
...options,
rejectUnauthorized: options.rejectUnauthorized ?? true,
};
};
super(tlsOptions);
this.timeline = Array.isArray(timeline) ? timeline : [];
this.alpnProtocols = options.ALPNProtocols || ['h2', 'http/1.1'];
@@ -131,6 +135,8 @@ function createTimelineAgentClass(BaseAgentClass) {
message: `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`,
});
}
this.caCertificatesCount = caCertificatesCount;
}
@@ -146,20 +152,16 @@ function createTimelineAgentClass(BaseAgentClass) {
});
}
// Log CAfile and CApath (if possible)
if (this.caProvided) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `CA certificates provided`,
});
} else {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `Using system default CA certificates`,
});
}
const rootCerts = this.caCertificatesCount.root || 0;
const systemCerts = this.caCertificatesCount.system || 0;
const extraCerts = this.caCertificatesCount.extra || 0;
const customCerts = this.caCertificatesCount.custom || 0;
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `CA Certificates: ${rootCerts} root, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`,
});
// Log "Trying host:port..."
this.timeline.push({

View File

@@ -0,0 +1,213 @@
const { transformRequestToSaveToFilesystem } = require('../collection');
describe('transformRequestToSaveToFilesystem', () => {
it('should preserve all relevant fields when transforming request', () => {
const testItem = {
uid: 'test-uid-123',
type: 'http-request',
name: 'Test Request',
seq: 1,
settings: {
enableEncodeUrl: true
},
tags: ['smoke', 'regression', 'api'],
request: {
method: 'POST',
url: 'https://api.example.com/test',
params: [
{
uid: 'param-uid-1',
name: 'param1',
value: 'value1',
description: 'Test parameter',
type: 'text',
enabled: true
}
],
headers: [
{
uid: 'header-uid-1',
name: 'Content-Type',
value: 'application/json',
description: 'Request content type',
enabled: true
}
],
auth: {
type: 'bearer',
token: 'test-token'
},
body: {
mode: 'json',
json: '{"test": "data"}'
},
script: {
req: 'console.log("request script");',
res: 'console.log("response script");'
},
vars: {
preRequest: 'const testVar = "value";',
postResponse: 'console.log(testVar);'
},
assertions: [
{
uid: 'assert-uid-1',
name: 'Status Code',
operator: 'equals',
expected: '200'
}
],
tests: [
{
uid: 'test-uid-1',
name: 'Test Response',
code: 'expect(response.status).toBe(200);'
}
],
docs: 'This is a test request documentation'
}
};
// Transform the request
const result = transformRequestToSaveToFilesystem(testItem);
// Verify all top-level fields are preserved
expect(result.uid).toBe(testItem.uid);
expect(result.type).toBe(testItem.type);
expect(result.name).toBe(testItem.name);
expect(result.seq).toBe(testItem.seq);
expect(result.settings).toEqual(testItem.settings);
// Verify tags are preserved (this is the main focus)
expect(result.tags).toEqual(['smoke', 'regression', 'api']);
expect(result.tags).toHaveLength(3);
// Verify request object structure
expect(result.request).toBeDefined();
expect(result.request.method).toBe(testItem.request.method);
expect(result.request.url).toBe(testItem.request.url);
expect(result.request.auth).toEqual(testItem.request.auth);
expect(result.request.body).toEqual(testItem.request.body);
expect(result.request.script).toEqual(testItem.request.script);
expect(result.request.vars).toEqual(testItem.request.vars);
expect(result.request.assertions).toEqual(testItem.request.assertions);
expect(result.request.tests).toEqual(testItem.request.tests);
expect(result.request.docs).toBe(testItem.request.docs);
// Verify params are processed correctly
expect(result.request.params).toHaveLength(1);
expect(result.request.params[0]).toEqual({
uid: 'param-uid-1',
name: 'param1',
value: 'value1',
description: 'Test parameter',
type: 'text',
enabled: true
});
// Verify headers are processed correctly
expect(result.request.headers).toHaveLength(1);
expect(result.request.headers[0]).toEqual({
uid: 'header-uid-1',
name: 'Content-Type',
value: 'application/json',
description: 'Request content type',
enabled: true
});
});
it('should handle draft items correctly', () => {
const testItem = {
uid: 'test-uid-456',
type: 'http-request',
name: 'Draft Request',
seq: 2,
settings: {},
tags: ['draft', 'wip'],
request: {
method: 'GET',
url: 'https://api.example.com/draft',
params: [],
headers: [],
auth: {},
body: { mode: 'none' },
script: { req: '', res: '' },
vars: { preRequest: '', postResponse: '' },
assertions: [],
tests: [],
docs: ''
},
draft: {
uid: 'draft-uid-789',
type: 'http-request',
name: 'Draft Request Modified',
seq: 2,
settings: { enableEncodeUrl: true },
tags: ['draft', 'wip', 'modified'],
request: {
method: 'PUT',
url: 'https://api.example.com/draft-modified',
params: [],
headers: [],
auth: {},
body: { mode: 'none' },
script: { req: '', res: '' },
vars: { preRequest: '', postResponse: '' },
assertions: [],
tests: [],
docs: ''
}
}
};
const result = transformRequestToSaveToFilesystem(testItem);
// Should use draft data when available
expect(result.uid).toBe('draft-uid-789');
expect(result.name).toBe('Draft Request Modified');
expect(result.settings).toEqual({ enableEncodeUrl: true });
// Verify draft tags are preserved
expect(result.tags).toEqual(['draft', 'wip', 'modified']);
expect(result.tags).toContain('modified');
expect(result.tags).toHaveLength(3);
});
it('should handle gRPC requests', () => {
const testItem = {
uid: 'grpc-uid-123',
type: 'grpc-request',
name: 'gRPC Test Request',
seq: 3,
settings: {},
tags: ['grpc', 'microservice'],
request: {
method: 'unary',
methodType: 'unary',
protoPath: '/path/to/proto',
url: 'grpc://localhost:50051',
params: [], // gRPC requests don't use params
headers: [],
auth: {},
body: { mode: 'grpc', grpc: [{ name: 'message1', content: 'test content' }] },
script: { req: '', res: '' },
vars: { preRequest: '', postResponse: '' },
assertions: [],
tests: [],
docs: 'gRPC test documentation'
}
};
const result = transformRequestToSaveToFilesystem(testItem);
// Verify gRPC-specific fields
expect(result.type).toBe('grpc-request');
expect(result.request.methodType).toBe('unary');
expect(result.request.protoPath).toBe('/path/to/proto');
expect(result.request.params).toBeUndefined(); // Should be deleted for gRPC
// Verify tags are preserved for gRPC requests
expect(result.tags).toEqual(['grpc', 'microservice']);
});
});

View File

@@ -2,10 +2,12 @@ const ScriptRuntime = require('./runtime/script-runtime');
const TestRuntime = require('./runtime/test-runtime');
const VarsRuntime = require('./runtime/vars-runtime');
const AssertRuntime = require('./runtime/assert-runtime');
const { runScriptInNodeVm } = require('./sandbox/node-vm');
module.exports = {
ScriptRuntime,
TestRuntime,
VarsRuntime,
AssertRuntime
AssertRuntime,
runScriptInNodeVm
};

View File

@@ -14,6 +14,7 @@ const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const { cleanJson } = require('../utils');
const { createBruTestResultMethods } = require('../utils/results');
const { runScriptInNodeVm } = require('../sandbox/node-vm');
// Inbuilt Library Support
const ajv = require('ajv');
@@ -111,6 +112,27 @@ class ScriptRuntime {
context.bru.runRequest = runRequestByItemPathname;
}
if (this.runtime === 'nodevm') {
await runScriptInNodeVm({
script,
context,
collectionPath,
scriptingConfig
});
return {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
persistentEnvVariables: bru.persistentEnvVariables,
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
results: cleanJson(__brunoTestResults.getResults()),
nextRequestName: bru.nextRequest,
skipRequest: bru.skipRequest,
stopExecution: bru.stopExecution
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,
@@ -260,6 +282,27 @@ class ScriptRuntime {
context.bru.runRequest = runRequestByItemPathname;
}
if (this.runtime === 'nodevm') {
await runScriptInNodeVm({
script,
context,
collectionPath,
scriptingConfig
});
return {
response,
envVariables: cleanJson(envVariables),
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
runtimeVariables: cleanJson(runtimeVariables),
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
results: cleanJson(__brunoTestResults.getResults()),
nextRequestName: bru.nextRequest,
skipRequest: bru.skipRequest,
stopExecution: bru.stopExecution
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,

View File

@@ -1,4 +1,5 @@
const { NodeVM } = require('@usebruno/vm2');
const { runScriptInNodeVm } = require('../sandbox/node-vm');
const chai = require('chai');
const path = require('path');
const http = require('http');
@@ -132,6 +133,13 @@ class TestRuntime {
script: testsFile,
context: context
});
} else if (this.runtime === 'nodevm') {
await runScriptInNodeVm({
script: testsFile,
context,
collectionPath,
scriptingConfig
});
} else {
// default runtime is vm2
const vm = new NodeVM({

View File

@@ -0,0 +1,223 @@
const vm = require('node:vm');
const fs = require('node:fs');
const path = require('node:path');
const { get } = require('lodash');
const lodash = require('lodash');
const { cleanJson } = require('../../utils');
class ScriptError extends Error {
constructor(error, script) {
super(error.message);
this.name = 'ScriptError';
this.originalError = error;
this.script = script;
this.stack = error.stack;
}
}
/**
* Executes a script in a Node.js VM context with enhanced security and module loading
* @param {Object} options - Configuration options
* @param {string} options.script - The script code to execute
* @param {Object} options.context - The execution context with Bruno objects
* @param {string} options.collectionPath - Path to the collection directory
* @param {Object} options.scriptingConfig - Scripting configuration options
* @returns {Promise<Object>} Execution results including variables and test results
* @throws {ScriptError} When script execution fails
*/
async function runScriptInNodeVm({
script,
context,
collectionPath,
scriptingConfig
}) {
if (script.trim().length === 0) {
return;
}
try {
// Create script context with all necessary variables
const scriptContext = {
// Bruno context
console: context.console,
req: context.req,
res: context.res,
bru: context.bru,
expect: context.expect,
assert: context.assert,
__brunoTestResults: context.__brunoTestResults,
test: context.test,
// Configuration for nested module loading
scriptingConfig: scriptingConfig,
// Global objects
Buffer: global.Buffer,
process: global.process,
setTimeout: global.setTimeout,
setInterval: global.setInterval,
clearTimeout: global.clearTimeout,
clearInterval: global.clearInterval,
setImmediate: global.setImmediate,
clearImmediate: global.clearImmediate
};
// Create shared cache for local modules
const localModuleCache = new Map();
// Create a custom require function and add it to the context
scriptContext.require = createCustomRequire({
scriptingConfig,
collectionPath,
scriptContext,
currentModuleDir: collectionPath,
localModuleCache
});
// Execute the script in an isolated VM context
await vm.runInNewContext(`
(async function(){
${script}
})();
`, scriptContext, {
filename: path.join(collectionPath, 'script.js'),
displayErrors: true
});
} catch (error) {
throw new ScriptError(error, script);
}
return;
}
/**
* Creates a custom require function with enhanced security and local module support
* @param {Object} options - Configuration options
* @param {Object} options.scriptingConfig - Scripting configuration with additional context roots
* @param {string} options.collectionPath - Base collection path for security checks
* @param {Object} options.scriptContext - Script execution context
* @param {string} options.currentModuleDir - Current module directory for relative imports
* @param {Map} options.localModuleCache - Cache for loaded local modules
* @returns {Function} Custom require function
*/
function createCustomRequire({
scriptingConfig,
collectionPath,
scriptContext,
currentModuleDir = collectionPath,
localModuleCache = new Map()
}) {
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
const additionalContextRootsAbsolute = lodash
.chain(additionalContextRoots)
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
.value();
additionalContextRootsAbsolute.push(collectionPath);
return (moduleName) => {
// Check if it's a local module (starts with ./ or ../)
if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
}
// First try to require as a native/npm module
try {
return require(moduleName);
} catch {
// If that fails, try to resolve from additionalContextRoots
try {
const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
return require(modulePath);
} catch (error) {
throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
}
}
};
}
/**
* Loads a local module from the filesystem with security checks and caching
* @param {Object} options - Configuration options
* @param {string} options.moduleName - Name/path of the module to load
* @param {string} options.collectionPath - Base collection path for security validation
* @param {Object} options.scriptContext - Script execution context to inherit
* @param {Map} options.localModuleCache - Cache for loaded modules
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
* @returns {*} The exported content of the loaded module
* @throws {Error} When module is outside collection path or cannot be loaded
*/
function loadLocalModule({
moduleName,
collectionPath,
scriptContext,
localModuleCache,
currentModuleDir
}) {
// Check if the filename has an extension
const hasExtension = path.extname(moduleName) !== '';
const resolvedFilename = hasExtension ? moduleName : `${moduleName}.js`;
// Resolve the file path relative to the current module's directory
const filePath = path.resolve(currentModuleDir, resolvedFilename);
const normalizedFilePath = path.normalize(filePath);
const normalizedCollectionPath = path.normalize(collectionPath);
// Cross-platform security check: ensure the resolved file is within collectionPath
const relativePath = path.relative(normalizedCollectionPath, normalizedFilePath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error(`Access to files outside of the collectionPath is not allowed: ${moduleName}`);
}
// Check cache first (use normalized path as key)
if (localModuleCache.has(normalizedFilePath)) {
return localModuleCache.get(normalizedFilePath);
}
if (!fs.existsSync(normalizedFilePath)) {
throw new Error(`Cannot find module ${moduleName}`);
}
// Read and execute the local module
const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
// Create module object
const moduleObj = { exports: {} };
// Get the directory of this module for nested imports
const moduleDir = path.dirname(normalizedFilePath);
// Create a new context that inherits from the script context
const moduleContext = {
...scriptContext,
module: moduleObj,
exports: moduleObj.exports,
__filename: normalizedFilePath,
__dirname: moduleDir,
// Create a custom require function for this module that resolves relative to its directory
require: createCustomRequire({
scriptingConfig: scriptContext.scriptingConfig || {},
collectionPath,
scriptContext,
currentModuleDir: moduleDir,
localModuleCache
})
};
try {
// Execute the module code in the shared context
vm.runInNewContext(moduleCode, moduleContext, {
filename: normalizedFilePath,
displayErrors: true
});
// Cache the result using normalized path
localModuleCache.set(normalizedFilePath, moduleObj.exports);
return moduleObj.exports;
} catch (error) {
throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
}
}
module.exports = {
runScriptInNodeVm
};

View File

@@ -1,6 +1,16 @@
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)*
@@ -10,14 +20,20 @@ const grammar = ohm.grammar(`Bru {
tagend = nl "}"
optionalnl = ~tagend nl
keychar = ~(tagend | st | nl | ":") any
valuechar = ~(nl | tagend) any
valuechar = ~(nl | tagend | multilinetextblockstart) any
multilinetextblockdelimiter = "'''"
multilinetextblockstart = "'''" nl
multilinetextblockend = nl st* "'''"
multilinetextblock = multilinetextblockstart multilinetextblockcontent multilinetextblockend
multilinetextblockcontent = (~multilinetextblockend any)*
// Dictionary Blocks
dictionary = st* "{" pairlist? tagend
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
pair = st* key st* ":" st* value st*
key = keychar*
value = valuechar*
value = multilinetextblock | valuechar*
// Array Blocks
array = st* "[" stnl* valuelist stnl* "]"
@@ -120,8 +136,31 @@ const sem = grammar.createSemantics().addAttribute('ast', {
return chars.sourceString ? chars.sourceString.trim() : '';
},
value(chars) {
// .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.ast
.split('\n')
.map((line) => line.slice(indentLevel)) // Remove 4-space indentation
.join('\n')
.trim();
},
multilinetextblockcontent(chars) {
return chars.sourceString;
},
nl(_1, _2) {
return '';
},

View File

@@ -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]);
@@ -16,23 +16,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,

View File

@@ -1,4 +1,5 @@
const _ = require('lodash');
const { getValueString, indentString } = require('./utils');
const envToJson = (json) => {
const variables = _.get(json, 'variables', []);
@@ -7,7 +8,8 @@ const envToJson = (json) => {
.map((variable) => {
const { name, value, enabled } = variable;
const prefix = enabled ? '' : '~';
return ` ${prefix}${name}: ${value}`;
return indentString(`${prefix}${name}: ${getValueString(value)}`);
});
const secretVars = variables
@@ -15,7 +17,7 @@ const envToJson = (json) => {
.map((variable) => {
const { name, enabled } = variable;
const prefix = enabled ? '' : '~';
return ` ${prefix}${name}`;
return indentString(`${prefix}${name}`);
});
if (!variables || !variables.length) {

View File

@@ -7,12 +7,21 @@ const safeParseJson = (json) => {
}
};
const normalizeNewlines = (str) => {
if (!str || typeof str !== 'string') {
return str || '';
}
// "\r\n" is windows, "\r" is old mac, "\n" is linux
return str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
};
const indentString = (str) => {
if (!str || !str.length) {
return str || '';
}
return str
return normalizeNewlines(str)
.split('\n')
.map((line) => ' ' + line)
.join('\n');
@@ -22,15 +31,33 @@ const outdentString = (str) => {
if (!str || !str.length) {
return str || '';
}
return str
return normalizeNewlines(str)
.split('\n')
.map((line) => line.replace(/^ /, ''))
.join('\n');
};
const getValueString = (value) => {
// Handle null, undefined, and empty strings
if (!value) {
return '';
}
const hasNewLines = value?.includes('\n') || value?.includes('\r');
if (!hasNewLines) {
return value;
}
// Wrap multiline values in triple quotes with 2-space indentation
return `'''\n${indentString(value)}\n'''`;
};
module.exports = {
safeParseJson,
normalizeNewlines,
indentString,
outdentString
outdentString,
getValueString
};

View File

@@ -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 that has 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: '''
<html>
<body>Hello World</body>
</html>
'''
}`;
const output = parser(input);
const expected = {
variables: [
{
name: 'config',
value: 'debug=true\nport=3000',
enabled: true,
secret: false
},
{
name: 'template',
value: '<html>\n <body>Hello World</body>\n</html>',
enabled: true,
secret: false
}
]
};
expect(output).toEqual(expected);
});
});

View File

@@ -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: [
{
@@ -137,6 +137,109 @@ vars:secret [
const expected = `vars:secret [
token
]
`;
expect(output).toEqual(expected);
});
it('should stringify multiline variables', () => {
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 stringify multiline variables containing 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 stringify 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 stringify multiple multiline variables', () => {
const input = {
variables: [
{
name: 'config',
value: 'debug=true\nport=3000',
enabled: true
},
{
name: 'template',
value: '<html>\n <body>Hello World</body>\n</html>',
enabled: true
}
]
};
const output = parser(input);
const expected = `vars {
config: '''
debug=true
port=3000
'''
template: '''
<html>
<body>Hello World</body>
</html>
'''
}
`;
expect(output).toEqual(expected);
});

View File

@@ -0,0 +1,21 @@
const { getValueString } = require('../src/utils');
describe('getValueString', () => {
it('returns single line value as-is', () => {
expect(getValueString('hello world')).toBe('hello world');
});
it('wraps multiline value in triple quotes with indentation', () => {
expect(getValueString('line1\nline2\nline3')).toBe("'''\n line1\n line2\n line3\n'''");
});
it('normalizes different newline types', () => {
expect(getValueString('line1\r\nline2\rline3\nline4')).toBe("'''\n line1\n line2\n line3\n line4\n'''");
});
it('returns empty string for empty/null/undefined', () => {
expect(getValueString('')).toBe('');
expect(getValueString(null)).toBe('');
expect(getValueString(undefined)).toBe('');
});
});

View File

@@ -27,8 +27,9 @@
"axios": "^1.9.0",
"grpc-reflection-js": "^0.3.0",
"is-ip": "^5.0.1",
"tough-cookie": "^6.0.0",
"ws": "^8.18.3"
"ws": "^8.18.3",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.22.0",

View File

@@ -38,6 +38,6 @@ module.exports = [
typescript({ tsconfig: './tsconfig.json' }),
terser()
],
external: ['axios', 'qs', 'ws']
external: ['axios', 'qs', 'ws', 'system-ca']
}
];

View File

@@ -3,6 +3,6 @@ export { GrpcClient, generateGrpcSampleMessage } from './grpc';
export { WsClient } from './ws/ws-client';
export { default as cookies } from './cookies';
export * as network from './network';
export { getCACertificates } from './utils/ca-cert';
export * as scripting from './scripting';

View File

@@ -0,0 +1,167 @@
import { systemCertsAsync, Options as SystemCAOptions } from 'system-ca';
import { rootCertificates } from 'node:tls';
import * as fs from 'node:fs';
type T_CACertificatesOptions = {
caCertFilePath?: string;
shouldKeepDefaultCerts?: boolean;
}
type T_CACertificatesResult = {
caCertificates: string;
caCertificatesCount: {
system: number;
root: number;
custom: number;
extra: number;
};
}
let systemCertsCache: string[] | undefined;
async function getSystemCerts(systemCAOpts: SystemCAOptions = {}): Promise<string[]> {
if (systemCertsCache) return systemCertsCache;
try {
systemCertsCache = await systemCertsAsync(systemCAOpts);
return systemCertsCache;
} catch (error) {
console.error(error);
return [];
}
}
function certToString(cert: string | Buffer) {
return typeof cert === 'string'
? cert
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
}
function mergeCA(...args: (string | string[])[]): string {
const ca = new Set<string>();
for (const item of args) {
if (!item) continue;
const caList = Array.isArray(item) ? item : [item];
for (const cert of caList) {
if (cert) {
ca.add(certToString(cert));
}
}
}
return [...ca].join('\n');
}
function getNodeExtraCACerts(): string[] {
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
if (!extraCACertPath) return [];
try {
if (fs.existsSync(extraCACertPath)) {
const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');
if (extraCACert && extraCACert.trim()) {
return [extraCACert];
}
}
} catch (err) {
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, (err as Error).message);
}
return [];
}
/**
* Get CA certificates
*
* Generic function to get CA certificates
* - System CA certificates (From OS)
* - Root CA certificates (From Node)
* - Custom CA certificates (From user-provided file)
* - NODE_EXTRA_CA_CERTS (From environment variable)
*
* If no custom CA certificate file path is provided
* → return system CA certificates and root certificates + NODE_EXTRA_CA_CERTS
*
* If custom CA certificate file path is provided
* → use custom CA certificate file + NODE_EXTRA_CA_CERTS
* → ignore system + root certificates if shouldKeepDefaultCerts is false
*
* @param caCertFilePath - path to custom CA certificate file
* @param shouldKeepDefaultCerts - whether to keep default CA certificates
* @returns {Promise<T_CACertificatesResult>} - CA certificates and their count
*/
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }: T_CACertificatesOptions): Promise<T_CACertificatesResult> => {
try {
let caCertificates = '';
let caCertificatesCount = {
system: 0,
root: 0,
custom: 0,
extra: 0
}
let systemCerts: string[] = [];
let rootCerts: string[] = [];
let customCerts: string[] = [];
let nodeExtraCerts: string[] = [];
// handle user-provided custom CA certificate file with optional default certificates
if (caCertFilePath) {
// validate custom CA certificate file
if (fs.existsSync(caCertFilePath)) {
try {
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
if (customCert && customCert.trim()) {
customCerts.push(customCert);
caCertificatesCount.custom = customCerts.length;
}
} catch (err) {
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err as Error).message);
throw new Error(`Unable to load custom CA certificate: ${(err as Error).message}`);
}
} else {
throw new Error(`Invalid custom CA certificate path: ${caCertFilePath}`);
}
if (shouldKeepDefaultCerts) {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
} else {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
// get NODE_EXTRA_CA_CERTS
nodeExtraCerts = getNodeExtraCACerts();
caCertificatesCount.extra = nodeExtraCerts.length;
// merge certs
const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);
caCertificates = mergedCerts;
return {
caCertificates,
caCertificatesCount
}
} catch (err) {
console.error('Error configuring CA certificates:', (err as Error).message);
throw err; // Re-throw certificate loading errors as they're critical
}
}
export {
getCACertificates
};

View File

@@ -1 +1 @@
v20
v22.17.0

View File

@@ -5,14 +5,14 @@ meta {
}
get {
url: https://httpbin.org/digest-auth/auth/foo/passwd
url: https://www.httpfaker.org/api/auth/digest/auth/admin/password
body: none
auth: digest
}
auth:digest {
username: foo
password: passwd
username: admin
password: password
}
assert {

View File

@@ -5,7 +5,7 @@ meta {
}
get {
url: https://httpbin.org/digest-auth/auth/foo/passw
url: https://www.httpfaker.org/api/auth/digest/auth/admin/badpassword
body: none
auth: digest
}

View File

@@ -19,16 +19,6 @@ script:post-response {
}
tests {
test("test body size", function() {
const bodySize = res.getSize().body;
expect(bodySize === 1048934).to.be.true;
});
test("test header size", function() {
const bodySize = res.getSize().header;
expect(bodySize === 305).to.be.true;
});
test("test total size", function() {
const sizes = res.getSize();
expect(sizes.total).to.equal(sizes.header + sizes.body);

View File

@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig } from '@playwright/test';
const reporter: any[] = [['list'], ['html']];
@@ -7,7 +7,6 @@ if (process.env.CI) {
}
export default defineConfig({
testDir: './e2e-tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
@@ -20,7 +19,15 @@ export default defineConfig({
projects: [
{
name: 'Bruno Electron App'
name: 'default',
testDir: './tests',
testIgnore: [
'ssl/**' // custom CA certificate tests require separate server setup and certificate generation
]
},
{
name: 'ssl',
testDir: './tests/ssl'
}
],
@@ -28,12 +35,14 @@ export default defineConfig({
{
command: 'npm run dev:web',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
reuseExistingServer: !process.env.CI,
timeout: 10 * 60 * 1000
},
{
command: 'npm start --workspace=packages/bruno-tests',
url: 'http://localhost:8081/ping',
reuseExistingServer: !process.env.CI
reuseExistingServer: !process.env.CI,
timeout: 10 * 60 * 1000
}
]
});

View File

@@ -5,7 +5,7 @@ async function main() {
const { app, context } = await startApp();
let outputFile = process.argv[2]?.trim();
if (outputFile && !/\.(ts|js)$/.test(outputFile)) {
outputFile = path.join(__dirname, '../e2e-tests/', outputFile + '.spec.ts');
outputFile = path.join(__dirname, '../tests/', outputFile + '.spec.ts');
}
await context._enableRecorder({ language: 'playwright-test', mode: 'recording', outputFile });
}

View File

@@ -4,7 +4,9 @@ const { _electron: electron } = require('playwright');
const electronAppPath = path.join(__dirname, '../packages/bruno-electron');
exports.startApp = async () => {
const app = await electron.launch({ args: [electronAppPath] });
const app = await electron.launch({
args: [electronAppPath]
});
const context = await app.context();
app.process().stdout.on('data', (data) => {

View File

@@ -48,7 +48,7 @@ export const test = baseTest.extend<
if (initUserDataPath) {
const replacements = {
projectRoot: path.join(__dirname, '..')
projectRoot: path.posix.join(__dirname, '..')
};
for (const file of await fs.promises.readdir(initUserDataPath)) {

128
scripts/count-locs.js Executable file
View File

@@ -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();

View File

@@ -1,6 +1,7 @@
import { test, expect } from '../../playwright';
import { test, expect } from '../../../playwright';
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
test('Create collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
// Create a new collection
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('test-collection');
@@ -8,24 +9,24 @@ test('Create new collection and add a simple HTTP request', async ({ page, creat
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
// Select safe mode
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new request
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('r1');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('http://localhost:8081');
await page.getByRole('button', { name: 'Create' }).click();
// Send a request
await page.locator('#request-url .CodeMirror').click();
await page.locator('textarea').fill('/ping');
await page.locator('#send-request').getByTitle('Save Request').click();
await page.locator('#send-request').getByRole('img').nth(2).click();
await expect(page.getByRole('main')).toContainText('200 OK');
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByText('GETr1').click();
await page.getByRole('button', { name: 'Clear response' }).click();
await page.locator('body').press('ControlOrMeta+Enter');
// Verify the response
await expect(page.getByRole('main')).toContainText('200 OK');
});

View File

@@ -0,0 +1,235 @@
import { test, expect } from '../../../playwright';
test.describe('Cross-Collection Drag and Drop for folder', () => {
test('Verify cross-collection folder drag and drop', async ({ pageWithUserData: page, createTmpDir }) => {
// Create first collection - click dropdown menu first
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('source-collection');
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Wait for collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a folder in the first collection
// Look for the collection menu button (usually three dots or similar)
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
// Fill folder name in the modal
await expect(page.locator('#collection-name')).toBeVisible();
await page.locator('#collection-name').fill('test-folder');
await page.getByRole('button', { name: 'Create' }).click();
// Wait for the folder to be created and appear in the sidebar
await page.waitForTimeout(2000);
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();
// Add a request to the folder to make it more realistic
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click({ button: 'right' });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('test-request-in-folder');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://httpbin.org/get');
await page.getByRole('button', { name: 'Create' }).click();
// Wait for the request to be created
await page.waitForTimeout(1000);
// Expand the folder to see the request inside
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();
await page.waitForTimeout(500);
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })).toBeVisible();
// Create second collection - click dropdown menu first
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('target-collection');
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Wait for second collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Wait for both collections to be visible in sidebar
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
// Locate the folder in source collection
const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });
await expect(sourceFolder).toBeVisible();
// Locate the target collection area (the collection name element)
const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });
await expect(targetCollection).toBeVisible();
// Perform drag and drop operation
await sourceFolder.dragTo(targetCollection);
// Wait for the operation to complete
await page.waitForTimeout(3000);
// Verify the folder has been moved to the target collection
// Click on target collection to expand it if needed
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
await page.waitForTimeout(1000);
// Check that the folder now appears under target collection
const targetCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' })
).toBeVisible();
// Expand the moved folder to verify the request inside is also moved
await targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();
await page.waitForTimeout(500);
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })
).toBeVisible();
// Verify the folder is no longer in the source collection
const sourceCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' })
).not.toBeVisible();
// Verify the request is also no longer in the source collection
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })
).not.toBeVisible();
});
test('Verify cross-collection folder drag and drop, a duplicate folder exist. expected to throw error toast', async ({
pageWithUserData: page,
createTmpDir
}) => {
// Create first collection (source) - use unique names for this test
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('source-collection');
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Wait for collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a folder in the first collection
await page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..')
.locator('.collection-actions')
.hover();
await page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..')
.locator('.collection-actions .icon')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await expect(page.locator('#collection-name')).toBeVisible();
await page.locator('#collection-name').fill('folder-1');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-1' })).toBeVisible();
// Add a request to the folder to make it more realistic
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click({ button: 'right' });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('http-request');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://httpbin.org/get');
await page.getByRole('button', { name: 'Create' }).click();
// Expand the folder to see the request inside
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'http-request' })).toBeVisible();
// Create second collection (target)
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('target-collection');
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Wait for second collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a folder with the same name in the target collection
await page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..')
.locator('.collection-actions')
.hover();
await page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..')
.locator('.collection-actions .icon')
.click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await expect(page.locator('#collection-name')).toBeVisible();
await page.locator('#collection-name').fill('folder-1');
await page.getByRole('button', { name: 'Create' }).click();
// Go back to source collection to drag the folder
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
// Verify we have the folder to drag in the source collection
const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).first();
await expect(sourceFolder).toBeVisible();
// Locate the target collection area
const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });
await expect(targetCollection).toBeVisible();
// Perform drag and drop operation
await sourceFolder.dragTo(targetCollection);
// check for error toast notification
await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible();
// source and target collection request should remain unchanged
const sourceCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' })
).toBeVisible();
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' })
).toBeVisible();
const targetCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' })
).toBeVisible();
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' })
).not.toBeVisible();
});
});

View File

@@ -0,0 +1,154 @@
import { test, expect } from '../../../playwright';
test.describe('Cross-Collection Drag and Drop', () => {
test('Verify request drag and drop', async ({ pageWithUserData: page, createTmpDir }) => {
// Create first collection - click dropdown menu first
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('source-collection');
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a request in the first collection
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('https://httpbin.org/get');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();
// Create second collection - click dropdown menu first
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('target-collection');
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
// Locate the request in source collection
const sourceRequest = page.locator('.collection-item-name').filter({ hasText: 'test-request' });
await expect(sourceRequest).toBeVisible();
// Locate the target collection area (the collection name element)
const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });
await expect(targetCollection).toBeVisible();
// Perform drag and drop operation
await sourceRequest.dragTo(targetCollection);
// Verify the request has been moved to the target collection
// Click on target collection to expand it if needed
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
// Check that the request now appears under target collection
const targetCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' })
).toBeVisible();
// Verify the request is no longer in the source collection
const sourceCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' })
).not.toBeVisible();
});
test('Expected to show error toast message, when duplicate request found in drop location', async ({
pageWithUserData: page,
createTmpDir
}) => {
// Create first collection (source-collection)
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('source-collection');
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Open collection
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a request in the first collection (request-1)
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('request-1');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://httpbin.org/get');
await page.getByRole('button', { name: 'Create' }).click();
// check if request-1 is created and visible in sidebar
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible();
// Create second collection (target-collection)
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('target-collection');
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Open collection
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a request in the target collection with the same name (request-1)
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('request-1');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://httpbin.org/post');
await page.getByRole('button', { name: 'Create' }).click();
// Go back to source collection to drag the request
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
const sourceRequest = page.locator('.collection-item-name').filter({ hasText: 'request-1' }).first();
await expect(sourceRequest).toBeVisible();
// Locate the target collection area
const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });
await expect(targetCollection).toBeVisible();
// Perform drag and drop operation to target-collection
await sourceRequest.dragTo(targetCollection);
// check for error toast notification
await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible();
// source and target collection request should remain unchanged
const targetCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' })
).toBeVisible();
const sourceCollectionContainer = page
.locator('.collection-name')
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' })
).toBeVisible();
});
});

View File

@@ -0,0 +1,149 @@
import { test, expect } from '../../../playwright';
test.describe('Tag persistence', () => {
test('Verify tag persistence while moving requests within a collection', async ({ pageWithUserData: page, createTmpDir }) => {
// Create first collection - click dropdown menu first
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new request
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByRole('textbox', { name: 'Request Name' }).fill('r1');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
// create another request
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByRole('textbox', { name: 'Request Name' }).fill('r2');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
// create another request
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByRole('textbox', { name: 'Request Name' }).fill('r3');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(200);
// Add a tag to the request
await page.getByRole('tab', { name: 'Settings' }).click();
await page.getByText('Tagse.g., smoke, regression').click();
await page.getByRole('textbox').nth(2).fill('smoke');
await page.getByRole('textbox').nth(2).press('Enter');
// Verify the tag was added
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
await page.keyboard.press('Meta+s');
// Move the r2 request to just above r1 within the same collection
const r3Request = page.locator('.collection-item-name').filter({ hasText: 'r3' });
const r1Request = page.locator('.collection-item-name').filter({ hasText: 'r1' });
await expect(r3Request).toBeVisible();
await expect(r1Request).toBeVisible();
// Perform drag and drop operation to move r3 below r1 using source position
await r3Request.dragTo(r1Request, {
targetPosition: { x: 0, y: 1 }
});
// Verify the requests are still in the collection and r3 is now above r1
await expect(page.locator('.collection-item-name').filter({ hasText: 'r3' })).toBeVisible();
await expect(page.locator('.collection-item-name').filter({ hasText: 'r1' })).toBeVisible();
// Click on r3 to verify the tag persisted after the move
await page.locator('.collection-item-name').filter({ hasText: 'r3' }).click();
await page.getByRole('tab', { name: 'Settings' }).click();
// Verify the tag is still present after the move
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
});
test('verify tag persistence while moving requests between folders', async ({ pageWithUserData: page, createTmpDir }) => {
// Create first collection - click dropdown menu first
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new folder
await page.getByTitle('test-collection').click({
button: 'right'
});
await page.waitForTimeout(200);
await page.getByText('New Folder').click();
await page.locator('#collection-name').fill('f1');
await page.getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(200);
// Create a new request within f1 folder
await page.getByText('f1').click();
await page.waitForTimeout(200);
await page.getByTitle('f1', { exact: true }).click({
button: 'right'
});
await page.locator('.dropdown-item').getByText('New Request').click()
await page.getByRole('textbox', { name: 'Request Name' }).fill('r1');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
// create another request within f1 folder
await page.getByTitle('f1', { exact: true }).click({
button: 'right'
});
await page.locator('.dropdown-item').getByText('New Request').click()
await page.getByRole('textbox', { name: 'Request Name' }).fill('r2');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
await page.waitForTimeout(200);
// Add a tag to the request
await page.getByRole('tab', { name: 'Settings' }).click();
await page.getByText('Tagse.g., smoke, regression').click();
await page.getByRole('textbox').nth(2).fill('smoke');
await page.getByRole('textbox').nth(2).press('Enter');
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
await page.keyboard.press('Meta+s');
// Create another folder
await page.getByTitle('test-collection').click({
button: 'right'
});
await page.locator('.dropdown-item').getByText('New Folder').click();
await page.locator('#collection-name').fill('f2');
await page.getByRole('button', { name: 'Create' }).click();
// open f2 folder
await page.getByText('f2').click();
await page.getByTitle('f2', { exact: true }).click({
button: 'right'
});
await page.locator('.dropdown-item').getByText('New Request').click();
await page.getByRole('textbox', { name: 'Request Name' }).fill('r3');
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
await page.getByRole('button', { name: 'Create' }).click();
// Drag and drop r2 request to f2 folder
const r2Request = page.locator('.collection-item-name').filter({ hasText: 'r2' });
const f2Folder = page.locator('.collection-item-name').filter({ hasText: 'f2' });
await r2Request.dragTo(f2Folder);
// Verify the requests are still in the collection and r2 is now in f2 folder
await expect(page.locator('.collection-item-name').filter({ hasText: 'r2' })).toBeVisible();
await expect(page.locator('.collection-item-name').filter({ hasText: 'f2' })).toBeVisible();
// Click on r2 to verify the tag persisted after the move
await page.locator('.collection-item-name').filter({ hasText: 'r2' }).click();
await page.getByRole('tab', { name: 'Settings' }).click();
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
});
});

View File

@@ -0,0 +1,57 @@
import { test, expect } from '../../../playwright';
import fs from 'fs';
import path from 'path';
test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
test.setTimeout(2 * 10 * 1000);
test('set env var with persist using script', async ({ pageWithUserData: page, restartApp }) => {
// Keep a copy of the original Stage.bru file
const originalStageBruPath = path.join(__dirname, 'collection/environments/Stage.bru');
const originalStageBruContent = fs.readFileSync(originalStageBruPath, 'utf8');
// Select the collection and request
await page.locator('#sidebar-collection-name').click();
await page.getByText('api-setEnvVar-with-persist', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
// select stage environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Stage' })).toBeVisible();
await page.locator('.dropdown-item').filter({ hasText: 'Stage' }).click();
await expect(page.locator('.current-environment').filter({ hasText: /Stage/ })).toBeVisible();
// Send the request
await page.locator('#send-request').getByRole('img').nth(2).click();
await page.waitForTimeout(1000);
// confirm that the environment variable is set
await page.getByTitle('Stage', { exact: true }).click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
await page.getByText('×').click();
// we restart the app to confirm that the environment variable is persisted
const newApp = await restartApp();
const newPage = await newApp.firstWindow();
// select the collection and request
await newPage.locator('#sidebar-collection-name').click();
await newPage.getByText('api-setEnvVar-with-persist', { exact: true }).click();
// open environment dropdown
await newPage.locator('div.current-environment.collection-environment').click();
await newPage.getByText('Configure', { exact: true }).click();
await expect(newPage.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
// close the environment modal
await newPage.getByText('×').click();
// Restore the original Stage.bru file
fs.writeFileSync(originalStageBruPath, originalStageBruContent);
await newPage.close();
});
});

View File

@@ -0,0 +1,49 @@
import { test, expect } from '../../../playwright';
test.describe.serial('bru.setEnvVar(name, value)', () => {
test.setTimeout(2 * 10 * 1000);
test('set env var using script', async ({ pageWithUserData: page, restartApp }) => {
// Select the collection and request
await page.locator('#sidebar-collection-name').click();
await page.getByText('api-setEnvVar-without-persist', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-environment').click();
// select stage environment
await expect(page.locator('.dropdown-item').filter({ hasText: 'Stage' })).toBeVisible();
await page.locator('.dropdown-item').filter({ hasText: 'Stage' }).click();
await expect(page.locator('.current-environment').filter({ hasText: /Stage/ })).toBeVisible();
// Send the request
await page.locator('#send-request').getByRole('img').nth(2).click();
await page.waitForTimeout(1000);
// confirm that the environment variable is set
await page.getByTitle('Stage', { exact: true }).click();
await page.getByText('Configure', { exact: true }).click();
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
await page.getByText('×').click();
// we restart the app to confirm that the environment variable is not persisted
const newApp = await restartApp();
const newPage = await newApp.firstWindow();
// select the collection and request
await newPage.locator('#sidebar-collection-name').click();
await newPage.getByText('api-setEnvVar-without-persist', { exact: true }).click();
// open environment dropdown
await newPage.locator('div.current-environment.collection-environment').click();
await newPage.getByText('Configure', { exact: true }).click();
// ensure that the environment variable is not persisted
await expect(newPage.locator('table.environment-variables tbody')).not.toContainText('token');
// close the environment variable modal
await newPage.getByText('×').click();
await newPage.close();
});
});

View File

@@ -1,5 +1,5 @@
meta {
name: ping
name: api-setEnvVar-with-persist
type: http
seq: 1
}
@@ -11,5 +11,5 @@ get {
}
script:pre-request {
bru.setEnvVar("persistent-env-test", "persistent-env-test-value", { persist: true });
bru.setEnvVar("token", "secret", { persist: true });
}

View File

@@ -1,5 +1,5 @@
meta {
name: ping2
name: api-setEnvVar-without-persist
type: http
seq: 1
}
@@ -11,5 +11,5 @@ get {
}
script:pre-request {
bru.setEnvVar("persistent-env-test", "persistent-env-test-value");
bru.setEnvVar("token", "secret");
}

View File

@@ -1,3 +1,4 @@
vars {
host: https://testbench-sanity.usebruno.com
}
token: secret
}

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/environments/api-setEnvVar/collection",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -0,0 +1,6 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/tests/environments/api-setEnvVar/collection"
]
}

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "multiline-variables",
"type": "collection"
}

View File

@@ -0,0 +1,5 @@
meta {
name: multiline-variables
type: collection
version: 1.0.0
}

View File

@@ -0,0 +1,8 @@
vars {
host: https://www.httpfaker.org
multiline_data: '''
line1
line2
line3
'''
}

View File

@@ -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");
});
}

View File

@@ -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");
});
}

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/environments/multiline-variables/collection",
"securityConfig": {
"jsSandboxMode": "developer"
}
}
]
}

View File

@@ -0,0 +1,28 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/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": ""
}
}

View File

@@ -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.getByTitle('request', { exact: true })).toBeVisible();
await page.getByTitle('request', { exact: true }).click();
// open environment dropdown
await page.locator('div.current-environment.collection-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"');
});
});

View File

@@ -0,0 +1,92 @@
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 page.locator('div.current-environment.collection-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);
});
});

Some files were not shown because too many files have changed in this diff Show More