mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 21:25:45 +00:00
Merge branch 'main' into feat/websocket-engine
This commit is contained in:
26
.github/actions/common/setup-node-deps/action.yml
vendored
Normal file
26
.github/actions/common/setup-node-deps/action.yml
vendored
Normal 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
|
||||
36
.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
36
.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml
vendored
Normal 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
|
||||
33
.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
33
.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal 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
|
||||
19
.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml
vendored
Normal file
19
.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml
vendored
Normal 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
|
||||
26
.github/actions/ssl/linux/setup-ca-certs/action.yml
vendored
Normal file
26
.github/actions/ssl/linux/setup-ca-certs/action.yml
vendored
Normal 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: $!"
|
||||
15
.github/actions/ssl/linux/setup-feature-specific-deps/action.yml
vendored
Normal file
15
.github/actions/ssl/linux/setup-feature-specific-deps/action.yml
vendored
Normal 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
|
||||
36
.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
36
.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml
vendored
Normal 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
|
||||
33
.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
33
.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal 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
|
||||
17
.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml
vendored
Normal file
17
.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml
vendored
Normal 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
|
||||
26
.github/actions/ssl/macos/setup-ca-certs/action.yml
vendored
Normal file
26
.github/actions/ssl/macos/setup-ca-certs/action.yml
vendored
Normal 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: $!"
|
||||
9
.github/actions/ssl/macos/setup-feature-specific-deps/action.yml
vendored
Normal file
9
.github/actions/ssl/macos/setup-feature-specific-deps/action.yml
vendored
Normal 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
|
||||
50
.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
50
.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml
vendored
Normal 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 }
|
||||
47
.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
47
.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal 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 }
|
||||
17
.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml
vendored
Normal file
17
.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml
vendored
Normal 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
|
||||
25
.github/actions/ssl/windows/setup-ca-certs/action.yml
vendored
Normal file
25
.github/actions/ssl/windows/setup-ca-certs/action.yml
vendored
Normal 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
|
||||
2
.github/workflows/npm-bru-cli.yml
vendored
2
.github/workflows/npm-bru-cli.yml
vendored
@@ -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
91
.github/workflows/ssl-tests.yml
vendored
Normal 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
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
470
docs/playwright-testing-guide.md
Normal file
470
docs/playwright-testing-guide.md
Normal 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.
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
persistent-env-test: persistent-env-test-value
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
|
||||
]
|
||||
}
|
||||
337
package-lock.json
generated
337
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -16,7 +16,7 @@ const StatusCode = ({ status }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getTabClassname(status)}>
|
||||
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`}>
|
||||
{status} {statusCodePhraseMap[status]}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useSelector } from 'react-redux';
|
||||
*/
|
||||
export const BETA_FEATURES = Object.freeze({
|
||||
GRPC: 'grpc',
|
||||
WEBSOCKET: 'websocket'
|
||||
WEBSOCKET: 'websocket',
|
||||
NODE_VM: 'nodevm'
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
213
packages/bruno-electron/src/utils/tests/collection-utils.spec.js
Normal file
213
packages/bruno-electron/src/utils/tests/collection-utils.spec.js
Normal 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']);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
223
packages/bruno-js/src/sandbox/node-vm/index.js
Normal file
223
packages/bruno-js/src/sandbox/node-vm/index.js
Normal 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
|
||||
};
|
||||
@@ -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 '';
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
21
packages/bruno-lang/v2/tests/utils.spec.js
Normal file
21
packages/bruno-lang/v2/tests/utils.spec.js
Normal 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('');
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -38,6 +38,6 @@ module.exports = [
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
],
|
||||
external: ['axios', 'qs', 'ws']
|
||||
external: ['axios', 'qs', 'ws', 'system-ca']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
167
packages/bruno-requests/src/utils/ca-cert.ts
Normal file
167
packages/bruno-requests/src/utils/ca-cert.ts
Normal 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
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
v20
|
||||
v22.17.0
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
128
scripts/count-locs.js
Executable 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();
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
149
tests/collection/moving-requests/tag-persistence.spec.ts
Normal file
149
tests/collection/moving-requests/tag-persistence.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
}
|
||||
token: secret
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/api-setEnvVar/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/api-setEnvVar/collection"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "multiline-variables",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
meta {
|
||||
name: multiline-variables
|
||||
type: collection
|
||||
version: 1.0.0
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
vars {
|
||||
host: https://www.httpfaker.org
|
||||
multiline_data: '''
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
'''
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/environments/multiline-variables/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "developer"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user