feat: Set JavaScript sandbox to safe mode by default for new collections (#4824)

* feat: Set JavaScript sandbox to safe mode by default for new collections

* rm: sandbox code in playwright test

* rm: safe mode code in var interpolation test

* rm: sandbox modal code

* fix

* fix

* fix

* fix

* improve

* improvement

* fix

* fix
This commit is contained in:
Pooja
2025-12-18 17:27:38 +05:30
committed by GitHub
parent bc2efb9686
commit 5e6444b8b5
44 changed files with 120 additions and 282 deletions

View File

@@ -2,11 +2,10 @@ import { useDispatch } from 'react-redux';
import { IconShieldCheck, IconCode } from '@tabler/icons';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { uuid } from 'utils/common/index';
import JsSandboxModeModal from '../JsSandboxModeModal';
import StyledWrapper from './StyledWrapper';
const JsSandboxMode = ({ collection }) => {
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode || 'safe';
const dispatch = useDispatch();
const viewSecuritySettings = () => {
@@ -41,7 +40,6 @@ const JsSandboxMode = ({ collection }) => {
<IconCode size={14} strokeWidth={2} />
</div>
)}
{!jsSandboxMode ? <JsSandboxModeModal collection={collection} /> : null}
</StyledWrapper>
);
};

View File

@@ -1,12 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -1,94 +0,0 @@
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import { useState } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const JsSandboxModeModal = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<Portal>
<Modal
size="sm"
title="JavaScript Sandbox"
confirmText="Save"
handleConfirm={handleSave}
hideCancel={true}
hideClose={true}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<StyledWrapper>
<div>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className="text-muted mt-6">
Please choose the security level for the JavaScript code execution.
</div>
<div className="flex flex-col mt-4">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
</label>
<p className="text-muted mt-1">
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className="ml-1 developer-mode-warning">(use only if you trust the authors of the collection)</span>
</span>
</label>
<p className="text-muted mt-1">
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
</div>
</StyledWrapper>
</Modal>
</Portal>
);
};
export default JsSandboxModeModal;

View File

@@ -22,7 +22,7 @@ import {
IconFolder
} from '@tabler/icons';
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder } from 'providers/ReduxStore/slices/collections/actions';
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder, saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
import { hideApiSpecPage, hideHomePage } from 'providers/ReduxStore/slices/app';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
@@ -102,6 +102,12 @@ const Collection = ({ collection, searchText }) => {
if (collection.collapsed) {
dispatch(toggleCollection(collection.uid));
// Set default jsSandboxMode to 'safe' if not present and save to disk
if (!collection.securityConfig?.jsSandboxMode) {
dispatch(saveCollectionSecurityConfig(collection.uid, {
jsSandboxMode: 'safe'
}));
}
}
if (!isChevronClick) {

View File

@@ -2,7 +2,7 @@ import { execSync } from 'child_process';
import { test, expect } from '../../../playwright';
import { Page, ElectronApplication } from '@playwright/test';
import path from 'path';
import { openCollectionAndAcceptSandbox } from '../../utils/page/actions';
import { openCollection } from '../../utils/page/actions';
import { buildCommonLocators } from '../../utils/page/locators';
/**
@@ -99,7 +99,7 @@ test.describe.skip('Close All Collections', () => {
});
await test.step('Create unsaved changes', async () => {
await openCollectionAndAcceptSandbox(page, 'collection 1');
await openCollection(page, 'collection 1');
await newLocators.sidebar.request('test-request').click();
const urlContainer = page.locator('#request-url');
@@ -129,7 +129,7 @@ test.describe.skip('Close All Collections', () => {
const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);
await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();
await openCollectionAndAcceptSandbox(restartedPage, 'collection 1');
await openCollection(restartedPage, 'collection 1');
await restartedLocators.sidebar.request('test-request').click();
const urlContainerAfterReopen = restartedPage.locator('#request-url');
@@ -148,7 +148,7 @@ test.describe.skip('Close All Collections', () => {
});
await test.step('Create unsaved changes', async () => {
await openCollectionAndAcceptSandbox(page, 'collection 1');
await openCollection(page, 'collection 1');
await newLocators.sidebar.request('test-request').click();
const urlContainer = page.locator('#request-url');
@@ -177,7 +177,7 @@ test.describe.skip('Close All Collections', () => {
const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);
await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();
await openCollectionAndAcceptSandbox(restartedPage, 'collection 1');
await openCollection(restartedPage, 'collection 1');
await restartedLocators.sidebar.request('test-request').click();
const urlContainerAfterReopen = restartedPage.locator('#request-url');

View File

@@ -20,10 +20,6 @@ test.describe('Create collection', () => {
await page.locator('.bruno-modal').getByRole('button', { name: 'Create', exact: true }).click();
await page.locator('#sidebar-collection-name').filter({ hasText: 'test-collection' }).click();
// Select safe mode
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new request using the new dropdown flow
await createUntitledRequest(page, { requestType: 'HTTP' });

View File

@@ -1,7 +1,7 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import * as fs from 'fs';
import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page';
import { closeAllCollections, openCollection } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
test.describe('Default ignores for node_modules and .git', () => {
@@ -84,7 +84,7 @@ get {
await expect(locators.sidebar.collection('Node Modules Ignore Test')).toBeVisible({ timeout: 30000 });
// Accept the sandbox mode
await openCollectionAndAcceptSandbox(page, 'Node Modules Ignore Test', 'safe');
await openCollection(page, 'Node Modules Ignore Test');
// Verify only the real request is visible
await expect(locators.sidebar.request('Real Request')).toBeVisible({ timeout: 10000 });
@@ -171,7 +171,7 @@ get {
await expect(locators.sidebar.collection('Git Ignore Test')).toBeVisible({ timeout: 30000 });
// Accept the sandbox mode
await openCollectionAndAcceptSandbox(page, 'Git Ignore Test', 'safe');
await openCollection(page, 'Git Ignore Test');
// Verify only the real request is visible
await expect(locators.sidebar.request('Real Git Request')).toBeVisible({ timeout: 10000 });

View File

@@ -0,0 +1,39 @@
import { test, expect } from '../../../playwright';
import { createCollection, openCollection } from '../../utils/page/actions';
test.describe('Default JavaScript Sandbox Mode', () => {
test('should set jsSandboxMode to safe by default when creating a new collection', async ({ page, createTmpDir }) => {
const collectionName = 'test-sandbox-collection';
await createCollection(page, collectionName, await createTmpDir());
// Verify sandbox mode is set to safe by default
const sandboxModeSelector = page.getByTestId('sandbox-mode-selector');
await expect(sandboxModeSelector).toBeVisible();
await expect(sandboxModeSelector).toHaveAttribute('title', 'Safe Mode');
// Click on sandbox mode selector to open security settings
await sandboxModeSelector.click();
// Change to developer mode
const developerRadio = page.locator('input[id="developer"]');
await developerRadio.click();
// Save
const saveButton = page.getByRole('button', { name: 'Save' });
await saveButton.click();
// Verify mode changed to developer
await expect(sandboxModeSelector).toHaveAttribute('title', 'Developer Mode');
// Close all tabs
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
await page.keyboard.press(`${modifier}+Shift+W`);
// Reopen the collection
await openCollection(page, collectionName);
// Verify mode is still developer (persisted)
await expect(sandboxModeSelector).toHaveAttribute('title', 'Developer Mode');
});
});

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createCollection, openCollectionAndAcceptSandbox } from '../../utils/page';
import { closeAllCollections, createCollection } from '../../utils/page';
test.describe('Draft indicator in collection and folder settings', () => {
test.afterAll(async ({ page }) => {
@@ -13,9 +13,6 @@ test.describe('Draft indicator in collection and folder settings', () => {
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
// Open collection settings by clicking on the collection name
await openCollectionAndAcceptSandbox(page, collectionName);
// Verify the collection settings tab is open
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { createCollection, openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page';
import { createCollection, closeAllCollections } from '../../utils/page';
test.describe('Draft values are used in requests', () => {
test.afterEach(async ({ page }) => {
@@ -12,7 +12,6 @@ test.describe('Draft values are used in requests', () => {
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
await openCollectionAndAcceptSandbox(page, collectionName);
// Verify the collection settings tab is open
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
@@ -119,7 +118,6 @@ test.describe('Draft values are used in requests', () => {
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
await openCollectionAndAcceptSandbox(page, collectionName);
// Create a new request from collection menu
const collection = page.locator('.collection-name').filter({ hasText: collectionName });

View File

@@ -9,7 +9,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
test('Verify cross-collection folder drag and drop', async ({ page, createTmpDir }) => {
// Create first collection - open with sandbox mode
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'source-collection', await createTmpDir('source-collection'));
// Create a folder in the first collection
// Look for the collection menu button for the source collection specifically
@@ -45,7 +45,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })).toBeVisible();
// Create second collection - open with sandbox mode
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'target-collection', await createTmpDir('target-collection'));
// Wait for both collections to be visible in sidebar
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
@@ -66,10 +66,6 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
await page.waitForTimeout(200);
// 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(200);
// Check that the folder now appears under target collection
const targetCollectionContainer = page
.locator('.collection-name')
@@ -106,7 +102,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
createTmpDir
}) => {
// Create first collection (source) - use unique names for this test
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'source-collection', await createTmpDir('source-collection'));
// Create a folder in the first collection
await page
@@ -141,7 +137,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
await expect(page.locator('.collection-item-name').filter({ hasText: 'http-request' })).toBeVisible();
// Create second collection (target)
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'target-collection', await createTmpDir('target-collection'));
// Create a folder with the same name in the target collection
await page
@@ -161,9 +157,6 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
await page.locator('#folder-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();

View File

@@ -9,7 +9,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
test('Verify request drag and drop', async ({ page, createTmpDir }) => {
// Create first collection - open with sandbox mode
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'source-collection', await createTmpDir('source-collection'));
// Create a request in the first collection using the new dropdown flow
await createUntitledRequest(page, { requestType: 'HTTP' });
@@ -22,7 +22,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
await expect(page.locator('.item-name').filter({ hasText: /^Untitled/ })).toBeVisible();
// Create second collection - open with sandbox mode
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'target-collection', await createTmpDir('target-collection'));
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
@@ -39,9 +39,6 @@ test.describe('Cross-Collection Drag and Drop', () => {
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')
@@ -66,7 +63,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
createTmpDir
}) => {
// Create first collection (source-collection)
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'source-collection', await createTmpDir('source-collection'));
// Create a request in the first collection using the new dropdown flow
await createUntitledRequest(page, { requestType: 'HTTP' });
@@ -80,7 +77,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
await expect(page.locator('.item-name').filter({ hasText: /^Untitled/ })).toBeVisible();
// Create second collection (target-collection)
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'target-collection', await createTmpDir('target-collection'));
// Create a request in the target collection using the new dropdown flow
await createUntitledRequest(page, { requestType: 'HTTP' });
@@ -91,7 +88,6 @@ test.describe('Cross-Collection Drag and Drop', () => {
await page.locator('#send-request').getByTitle('Save Request').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('.item-name').filter({ hasText: /^Untitled/ }).first();
await expect(sourceRequest).toBeVisible();

View File

@@ -20,8 +20,6 @@ test.describe('Tag persistence', () => {
}
await locators.modal.button('Create').click();
await locators.sidebar.collection('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.waitForTimeout(1000);
// Create three requests, each with URL and tag (auto-saved after each is completely created)
// The createUntitledRequest function now waits for each request to be fully created
@@ -82,8 +80,6 @@ test.describe('Tag persistence', () => {
}
await locators.modal.button('Create').click();
await locators.sidebar.collection('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new folder
await locators.sidebar.collectionRow('test-collection').hover();

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections } from '../../utils/page';
import { closeAllCollections, openCollection } from '../../utils/page';
test.describe('Move tabs', () => {
test.afterEach(async ({ page }) => {
@@ -21,8 +21,7 @@ test.describe('Move tabs', () => {
// Wait for collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection-drag-drop' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection-drag-drop' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await openCollection(page, 'source-collection-drag-drop');
// Create a folder in the collection
const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection-drag-drop' });
@@ -112,8 +111,6 @@ test.describe('Move tabs', () => {
// Wait for collection to appear and click on it
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection-keyboard-shortcut' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection-keyboard-shortcut' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a folder in the collection
const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection-keyboard-shortcut' });

View File

@@ -21,8 +21,7 @@ test.describe('Collection Environment Create Tests', () => {
await test.step('Import collection', async () => {
await importCollection(page, collectionFile, await createTmpDir('env-test'), {
expectedCollectionName: 'test_collection',
openWithSandboxMode: 'safe'
expectedCollectionName: 'test_collection'
});
});

View File

@@ -23,8 +23,7 @@ test.describe('Global Environment Create Tests', () => {
await test.step('Import collection', async () => {
await importCollection(page, collectionFile, await createTmpDir('global-env-test'), {
expectedCollectionName: 'test_collection',
openWithSandboxMode: 'safe'
expectedCollectionName: 'test_collection'
});
});

View File

@@ -33,8 +33,6 @@ test.describe('Collection Environment Import Tests', () => {
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Import collection environment
await page.locator('[data-testid="environment-selector-trigger"]').click();

View File

@@ -28,8 +28,6 @@ test.describe('Global Environment Import Tests', () => {
// Configure collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Import global environment
await page.getByTestId('environment-selector-trigger').click();

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../playwright';
import { openCollectionAndAcceptSandbox, closeAllCollections, sendRequest, addEnvironmentVariables } from '../utils/page';
import { openCollection, closeAllCollections, sendRequest, addEnvironmentVariables } from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
test.describe('Global Environment Variables - Non-string Values', () => {
@@ -11,7 +11,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
test('should seed non-string globals via request and verify read-only + tooltip', async ({
pageWithUserData: page
}) => {
await openCollectionAndAcceptSandbox(page, 'global-env-non-string', 'safe');
await openCollection(page, 'global-env-non-string');
await test.step('Create a new global environment with a string variable', async () => {
await page.getByTestId('environment-selector-trigger').click();

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions';
import { openCollection, closeAllCollections } from '../../utils/page/actions';
test.describe('Import Insomnia v4 Collection - Environment Import', () => {
test.afterEach(async ({ page }) => {
@@ -42,7 +42,7 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => {
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4 with Environments')).toBeVisible();
await openCollectionAndAcceptSandbox(page, 'Test API Collection v4 with Environments', 'safe');
await openCollection(page, 'Test API Collection v4 with Environments');
});
await test.step('Open collection environments panel', async () => {

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions';
import { openCollection, closeAllCollections } from '../../utils/page/actions';
test.describe('Import Insomnia v5 Collection - Environment Import', () => {
test.afterEach(async ({ page }) => {
@@ -39,7 +39,7 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => {
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-env-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await openCollectionAndAcceptSandbox(page, 'Test API Collection v5 with Environments', 'safe');
await openCollection(page, 'Test API Collection v5 with Environments');
});
await test.step('Open collection environments panel', async () => {

View File

@@ -37,8 +37,6 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
// configure the collection settings
await page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// verify that all 3 requests were imported correctly despite duplicate operation names
await expect(page.locator('#collection-duplicate-test-collection .collection-item-name')).toHaveCount(3);

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page';
import { closeAllCollections, openCollection } from '../../utils/page';
test.describe('Import OpenAPI Collection with Examples', () => {
let originalShowOpenDialog;
@@ -78,7 +78,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
});
await test.step('Handle sandbox modal', async () => {
await openCollectionAndAcceptSandbox(page, 'API with Examples', 'safe');
await openCollection(page, 'API with Examples');
});
await test.step('Verify collection name appears in sidebar', async () => {
@@ -206,7 +206,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
});
await test.step('Handle sandbox modal', async () => {
await openCollectionAndAcceptSandbox(page, 'API with Examples', 'safe');
await openCollection(page, 'API with Examples');
});
await test.step('Verify collection name appears in sidebar', async () => {

View File

@@ -36,8 +36,6 @@ test.describe('OpenAPI Newline Handling', () => {
// configure the collection settings
await page.locator('#sidebar-collection-name').getByText('Newline Test Collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// verify that all requests were imported correctly despite newlines in operation names
// the parser should clean up the operation names and create valid request names

View File

@@ -40,8 +40,6 @@ test.describe('OpenAPI Path-Based Grouping', () => {
// Configure the collection settings
await page.locator('#sidebar-collection-name').getByText('Path Grouping Test API').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Verify path-based folder structure was created
// Should have 'users' and 'products' folders

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page';
import { closeAllCollections, openCollection } from '../../utils/page';
test.describe('Import Postman Collection with Examples', () => {
let originalShowOpenDialog;
@@ -81,8 +81,8 @@ test.describe('Import Postman Collection with Examples', () => {
await locationModal.getByRole('button', { name: 'Import' }).click();
});
await test.step('Handle sandboox modal', async () => {
await openCollectionAndAcceptSandbox(page, 'collection with examples', 'safe');
await test.step('Open collection', async () => {
await openCollection(page, 'collection with examples');
});
await test.step('Verify collection name appears in sidebar', async () => {

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page/actions';
import { closeAllCollections, openCollection } from '../../utils/page/actions';
test.describe('Import WSDL Collection', () => {
const testDataDir = path.join(__dirname, 'fixtures');
@@ -45,7 +45,7 @@ test.describe('Import WSDL Collection', () => {
await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();
// open the collection and accept the sandbox modal
await openCollectionAndAcceptSandbox(page, 'TestWSDLServiceXML', 'safe');
await openCollection(page, 'TestWSDLServiceXML');
// verify that all requests were imported correctly
await expect(page.locator('#collection-testwsdlservicexml .collection-item-name')).toHaveCount(1);
@@ -105,7 +105,7 @@ test.describe('Import WSDL Collection', () => {
await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceJSON')).toBeVisible();
// open the collection and accept the sandbox modal
await openCollectionAndAcceptSandbox(page, 'TestWSDLServiceJSON', 'safe');
await openCollection(page, 'TestWSDLServiceJSON');
// verify that all requests were imported correctly
await expect(page.locator('#collection-testwsdlservicejson .collection-item-name')).toHaveCount(1);

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, openCollectionAndAcceptSandbox, sendRequest } from '../../utils/page';
import { closeAllCollections, openCollection, sendRequest } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
test.describe.serial('Dynamic Variable Interpolation', () => {
@@ -11,7 +11,7 @@ test.describe.serial('Dynamic Variable Interpolation', () => {
const locators = buildCommonLocators(page);
// Open collection and accept sandbox mode
await openCollectionAndAcceptSandbox(page, 'dynamic-variable-interpolation', 'safe');
await openCollection(page, 'dynamic-variable-interpolation');
// Navigate to the request
await locators.sidebar.request('set-var-dynamic-variable').click();

View File

@@ -18,9 +18,6 @@ test.describe('Onboarding', () => {
// Click on the sample collection to open it
await sampleCollection.click();
const modeSaveButton = page.getByRole('button', { name: 'Save' });
await expect(modeSaveButton).toBeVisible();
await modeSaveButton.click();
// Verify the sample request is visible and clickable
const request = page.locator('.collection-item-name').getByText('Get Users');
@@ -44,9 +41,6 @@ test.describe('Onboarding', () => {
const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');
await expect(sampleCollection).toBeVisible();
await sampleCollection.click();
const modeSaveButton = page.getByRole('button', { name: 'Save' });
await expect(modeSaveButton).toBeVisible();
await modeSaveButton.click();
// Verify the sample request
const request = page.locator('.collection-item-name').getByText('Get Users');

View File

@@ -15,9 +15,7 @@ test.describe('Autosave', () => {
const collectionName = 'autosave-test';
await test.step('Create collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('autosave-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('autosave-collection'));
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
await createRequest(page, 'Test Request', collectionName);
@@ -127,9 +125,7 @@ test.describe('Autosave', () => {
const collectionName = 'autosave-existing-drafts-test';
await test.step('Create collection and request with initial URL', async () => {
await createCollection(page, collectionName, await createTmpDir('autosave-existing-drafts-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('autosave-existing-drafts-collection'));
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
await createRequest(page, 'Draft Request', collectionName);

View File

@@ -7,7 +7,7 @@ test.describe('Copy and Paste Folders', () => {
});
test('should copy and paste a folder within the same collection', async ({ page, createTmpDir }) => {
await createCollection(page, 'test-collection', await createTmpDir('test-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'test-collection', await createTmpDir('test-collection'));
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
// Create a new folder with a request inside
@@ -48,7 +48,7 @@ test.describe('Copy and Paste Folders', () => {
test('should copy and paste a folder into a different collection', async ({ page, createTmpDir }) => {
// Create second collection
await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'));
const collection2 = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });
// Paste the folder from clipboard into the new collection

View File

@@ -7,7 +7,7 @@ test.describe('Copy and Paste Requests', () => {
});
test('should copy and paste a request within the same collection', async ({ page, createTmpDir }) => {
await createCollection(page, 'test-collection', await createTmpDir('test-collection'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'test-collection', await createTmpDir('test-collection'));
// Create a new request
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
@@ -55,7 +55,7 @@ test.describe('Copy and Paste Requests', () => {
});
test('should copy and paste a request into a different collection', async ({ page, createTmpDir }) => {
await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'));
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });
// Paste into the collection root

View File

@@ -7,7 +7,7 @@ test.describe('Copy and Paste with Keyboard Shortcuts', () => {
});
test('should copy and paste request using keyboard shortcuts', async ({ page, createTmpDir }) => {
await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'), { openWithSandboxMode: 'safe' });
await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'));
const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });
// Create a request

View File

@@ -10,7 +10,7 @@ test.describe('Delete Request Sequence Updation', () => {
const collectionName = 'test-collection';
// Create a collection
await createCollection(page, collectionName, await createTmpDir(collectionName), { openWithSandboxMode: 'safe' });
await createCollection(page, collectionName, await createTmpDir(collectionName));
// Create request-a
await createRequest(page, 'request-a', collectionName);

View File

@@ -30,8 +30,6 @@ test.describe('Code Generation URL Encoding', () => {
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'unencoded-test-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'unencoded-test-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new request using the new dropdown flow
await createUntitledRequest(page, {
@@ -75,8 +73,6 @@ test.describe('Code Generation URL Encoding', () => {
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'encoded-test-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'encoded-test-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
// Create a new request using the new dropdown flow
await createUntitledRequest(page, {

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { openCollectionAndAcceptSandbox } from '../../utils/page/actions';
import { openCollection } from '../../utils/page';
import { getTableCell } from '../../utils/page/locators';
test('should persist request with newlines across app restarts', async ({ createTmpDir, launchElectronApp }) => {
@@ -16,8 +16,6 @@ test('should persist request with newlines across app restarts', async ({ create
await page.locator('.bruno-modal').getByLabel('Location').fill(collectionPath);
await page.locator('.bruno-modal').getByRole('button', { name: 'Create' }).click();
await openCollectionAndAcceptSandbox(page, 'newlines-persistence', 'safe');
const collection = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'newlines-persistence' });
await collection.hover();
await collection.locator('.collection-actions .icon').click();
@@ -27,6 +25,8 @@ test('should persist request with newlines across app restarts', async ({ create
await page.locator('#new-request-url').locator('textarea').fill('https://httpbin.org/get');
await page.locator('.bruno-modal').getByRole('button', { name: 'Create', exact: true }).click();
await openCollection(page, 'newlines-persistence');
await page.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
await page.getByRole('tab', { name: 'Params' }).click();

View File

@@ -19,8 +19,6 @@ const setup = async (page: Page, createTmpDir: (tag?: string | undefined) => Pro
await page.locator('.bruno-modal').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();
const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection' });
await sourceCollection.hover();
await sourceCollection.locator('.collection-actions .icon').click();

View File

@@ -14,8 +14,8 @@ test.describe('Large Response Crash/High Memory Usage Prevention', () => {
test('Show appropriate warning for responses over 10MB', async ({ page, createTmpDir }) => {
const collectionName = 'size-warning-test';
// Create collection
await createCollection(page, collectionName, await createTmpDir(collectionName), { openWithSandboxMode: 'safe' });
// Create collection (auto-opens the collection)
await createCollection(page, collectionName, await createTmpDir(collectionName));
// Create request using the new dropdown flow
await createUntitledRequest(page, {

View File

@@ -18,7 +18,7 @@ test.describe('Response Pane Actions', () => {
const locators = buildCommonLocators(page);
await test.step('Create collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir(collectionName), { openWithSandboxMode: 'safe' });
await createCollection(page, collectionName, await createTmpDir(collectionName));
await createRequest(page, 'copy-test', collectionName, { url: 'https://testbench-sanity.usebruno.com/ping' });
});

View File

@@ -25,8 +25,6 @@ test.describe.parallel('Collection Run', () => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'bruno-testbench' });

View File

@@ -1,5 +1,5 @@
import { test, expect } from '../../../../../playwright';
import { openCollectionAndAcceptSandbox } from '../../../../utils/page';
import { openCollection } from '../../../../utils/page';
import { buildWebsocketCommonLocators } from '../../../../utils/page/locators';
const BRU_REQ_NAME = /^ws-ssl-request$/;
@@ -12,7 +12,7 @@ test.describe.serial('wss with custom ca cert', () => {
const requestItem = page.getByTitle(BRU_REQ_NAME);
await test.step('Open collection', async () => {
await openCollectionAndAcceptSandbox(page, 'wss-custom-ca-certs-test', 'safe');
await openCollection(page, 'wss-custom-ca-certs-test');
});
await test.step('Connect to WSS', async () => {

View File

@@ -33,28 +33,14 @@ const closeAllCollections = async (page) => {
* Open a collection from the sidebar and accept the JavaScript Sandbox modal
* @param page - The page object
* @param collectionName - The name of the collection to open
* @param sandboxMode - The mode to accept the sandbox modal
* @returns void
*/
const openCollectionAndAcceptSandbox = async (page, collectionName: string, sandboxMode: 'safe' | 'developer' = 'safe') => {
await test.step(`Open collection "${collectionName}" and accept sandbox "${sandboxMode}" mode`, async () => {
const openCollection = async (page, collectionName: string) => {
await test.step(`Open collection "${collectionName}"`, async () => {
await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();
const sandboxModal = page
.locator('.bruno-modal-card')
.filter({ has: page.locator('.bruno-modal-header-title', { hasText: 'JavaScript Sandbox' }) });
const modeLabel = sandboxMode === 'safe' ? 'Safe Mode' : 'Developer Mode';
await sandboxModal.getByLabel(modeLabel).check();
await sandboxModal.locator('.bruno-modal-footer .submit').click();
await sandboxModal.waitFor({ state: 'detached' });
});
};
type CreateCollectionOptions = {
openWithSandboxMode?: 'safe' | 'developer';
};
/**
* Create a collection
* @param page - The page object
@@ -64,7 +50,7 @@ type CreateCollectionOptions = {
*
* @returns void
*/
const createCollection = async (page, collectionName: string, collectionLocation: string, options: CreateCollectionOptions = {}) => {
const createCollection = async (page, collectionName: string, collectionLocation: string) => {
await test.step(`Create collection "${collectionName}"`, async () => {
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
@@ -80,10 +66,7 @@ const createCollection = async (page, collectionName: string, collectionLocation
await createCollectionModal.waitFor({ state: 'detached', timeout: 15000 });
await page.waitForTimeout(200);
if (options.openWithSandboxMode != undefined) {
await openCollectionAndAcceptSandbox(page, collectionName, options.openWithSandboxMode);
}
await openCollection(page, collectionName);
});
};
@@ -238,7 +221,6 @@ const deleteRequest = async (page, requestName: string, collectionName: string)
*/
type ImportCollectionOptions = {
expectedCollectionName?: string;
openWithSandboxMode?: SandboxMode;
};
const importCollection = async (
@@ -280,11 +262,10 @@ const importCollection = async (
await expect(
page.locator('#sidebar-collection-name').filter({ hasText: options.expectedCollectionName })
).toBeVisible();
}
// Configure sandbox mode if requested
if (options.openWithSandboxMode) {
await openCollectionAndAcceptSandbox(page, options.expectedCollectionName, options.openWithSandboxMode);
}
if (options.expectedCollectionName) {
await openCollection(page, options.expectedCollectionName);
}
});
};
@@ -725,7 +706,7 @@ const clickResponseAction = async (page: Page, actionTestId: string) => {
export {
closeAllCollections,
openCollectionAndAcceptSandbox,
openCollection,
createCollection,
createRequest,
createUntitledRequest,
@@ -753,4 +734,4 @@ export {
clickResponseAction
};
export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };
export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };

View File

@@ -21,9 +21,7 @@ test.describe('Variable Tooltip', () => {
const collectionName = 'tooltip-test';
await test.step('Create collection and add environment variables', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('tooltip-collection'));
await createEnvironment(page, 'Test Env', 'collection');
@@ -111,9 +109,7 @@ test.describe('Variable Tooltip', () => {
const collectionName = 'tooltip-reference-test';
await test.step('Create collection with interdependent variables', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-ref-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('tooltip-ref-collection'));
await createEnvironment(page, 'Ref Test Env', 'collection');
@@ -231,9 +227,7 @@ test.describe('Variable Tooltip', () => {
const collectionName = 'tooltip-readonly-test';
await test.step('Create collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-readonly-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('tooltip-readonly-collection'));
await createEnvironment(page, 'Readonly Env', 'collection');
await saveEnvironment(page);
@@ -284,9 +278,7 @@ test.describe('Variable Tooltip', () => {
const collectionName = 'draft-autosave-test';
await test.step('Setup collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('draft-autosave'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('draft-autosave'));
// Create request using utility method
await createRequest(page, 'Autosave Test', collectionName);
@@ -394,9 +386,7 @@ test.describe('Variable Tooltip', () => {
const collectionName = 'invalid-var-test';
await test.step('Setup collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('invalid-var-collection'), {
openWithSandboxMode: 'safe'
});
await createCollection(page, collectionName, await createTmpDir('invalid-var-collection'));
// Create request using utility method
await createRequest(page, 'Invalid Var Test', collectionName);

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../playwright';
import { buildWebsocketCommonLocators } from '../../utils/page/locators';
import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page';
import { closeAllCollections, openCollection } from '../../utils/page';
const BRU_REQ_NAME = /^ws-interpolation-test$/;
const MAX_CONNECTION_TIME = 10000; // Increased timeout for external server
@@ -14,7 +14,7 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
const locators = buildWebsocketCommonLocators(page);
// Open the collection and accept sandbox modal if it appears
await openCollectionAndAcceptSandbox(page, 'variable-interpolation', 'safe');
await openCollection(page, 'variable-interpolation');
// Open the request
await expect(page.getByTitle(BRU_REQ_NAME)).toBeVisible();
@@ -50,21 +50,8 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
// Wait for collection to be visible (it should auto-load from preferences)
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' })).toBeVisible({ timeout: 5000 });
// Check if sandbox modal is present and handle it
const sandboxModal = page.locator('.bruno-modal-card').filter({ has: page.locator('.bruno-modal-header-title', { hasText: 'JavaScript Sandbox' }) });
const isModalVisible = await sandboxModal.isVisible().catch(() => false);
if (isModalVisible) {
// Accept sandbox modal
await sandboxModal.getByLabel('Safe Mode').check();
await sandboxModal.locator('.bruno-modal-footer .submit').click();
await sandboxModal.waitFor({ state: 'detached' });
} else {
// Collection might already be open, just ensure it's clicked
await page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' }).click();
}
// Wait a bit for any modals to fully close
// Click to expand the collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' }).click();
await page.waitForTimeout(300);
// Open the request