@@ -110,10 +398,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
onSave(finalValue)}
theme={storedTheme}
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
+ onPaste={item.type === 'http-request' ? handleHttpPaste : item.type === 'graphql-request' ? handleGraphqlPaste : null}
collection={collection}
highlightPathParams={true}
item={item}
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index 871258c42..a98217240 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -10,6 +10,7 @@ import CollectionToolBar from './CollectionToolBar';
import RequestTab from './RequestTab';
import StyledWrapper from './StyledWrapper';
import DraggableTab from './DraggableTab';
+import CreateUntitledRequest from 'components/CreateUntitledRequest';
const RequestTabs = () => {
const dispatch = useDispatch();
@@ -78,8 +79,6 @@ const RequestTabs = () => {
);
};
- const createNewTab = () => setNewRequestModalOpen(true);
-
if (!activeTabUid) {
return null;
}
@@ -178,19 +177,16 @@ const RequestTabs = () => {
) : null}
-
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 04331b527..64ff1740b 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -1629,3 +1629,53 @@ export const isVariableSecret = (scopeInfo) => {
return false;
};
+
+/**
+ * Generate a unique request name by checking existing filenames in the collection and filesystem
+ * @param {Object} collection - The collection object
+ * @param {string} baseName - The base name (default: 'Untitled')
+ * @param {string} itemUid - The parent item UID (null for root level, folder UID for folder level)
+ * @returns {Promise} - A unique request name (Untitled, Untitled1, Untitled2, etc.)
+ */
+export const generateUniqueRequestName = async (collection, baseName = 'Untitled', itemUid = null) => {
+ if (!collection) {
+ return baseName;
+ }
+
+ const trim = require('lodash/trim');
+ const parentItem = itemUid ? findItemInCollection(collection, itemUid) : null;
+ const parentItems = parentItem ? (parentItem.items || []) : (collection.items || []);
+ const baseNamePattern = new RegExp(`^${baseName}(\\d+)?$`);
+ // Support .bru, .yml, and .yaml file extensions
+ const requestExtensions = /\.(bru|yml|yaml)$/i;
+ const matchingItems = parentItems
+ .filter((item) => {
+ if (item.type === 'folder') return false;
+
+ const filename = trim(item.filename);
+ if (!requestExtensions.test(filename)) return false;
+
+ const filenameWithoutExt = filename.replace(requestExtensions, '');
+ return baseNamePattern.test(filenameWithoutExt);
+ })
+ .map((item) => {
+ const filenameWithoutExt = trim(item.filename).replace(requestExtensions, '');
+ const match = filenameWithoutExt.match(baseNamePattern);
+
+ if (!match) return null;
+
+ const number = match[1] ? parseInt(match[1], 10) : 0;
+ return { name: filenameWithoutExt, number: isNaN(number) ? null : number };
+ })
+ .filter((item) => item !== null && item.number !== null);
+
+ if (matchingItems.length === 0) {
+ return baseName;
+ }
+
+ const sortedMatches = matchingItems.sort((a, b) => a.number - b.number);
+ const lastElement = sortedMatches[sortedMatches.length - 1];
+ const nextNumber = lastElement.number + 1;
+
+ return `${baseName}${nextNumber}`;
+};
diff --git a/tests/collection/create/create-collection.spec.ts b/tests/collection/create/create-collection.spec.ts
index 5620e8af9..0e31277f1 100644
--- a/tests/collection/create/create-collection.spec.ts
+++ b/tests/collection/create/create-collection.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
-import { closeAllCollections } from '../../utils/page';
+import { closeAllCollections, createUntitledRequest } from '../../utils/page';
test.describe('Create collection', () => {
test.afterEach(async ({ page }) => {
@@ -24,12 +24,13 @@ test.describe('Create collection', () => {
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();
+ // Create a new request using the new dropdown flow
+ await createUntitledRequest(page, { requestType: 'HTTP' });
+
+ // Set the URL
+ await page.locator('#request-url .CodeMirror').click();
await page.locator('textarea').fill('http://localhost:8081');
- await page.getByRole('button', { name: 'Create' }).click();
+ await page.locator('#send-request').getByTitle('Save Request').click();
// Send a request
await page.locator('#request-url .CodeMirror').click();
diff --git a/tests/collection/moving-requests/cross-collection-drag-drop-request.spec.ts b/tests/collection/moving-requests/cross-collection-drag-drop-request.spec.ts
index 7c6e2438c..47120acad 100644
--- a/tests/collection/moving-requests/cross-collection-drag-drop-request.spec.ts
+++ b/tests/collection/moving-requests/cross-collection-drag-drop-request.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
-import { closeAllCollections, createCollection } from '../../utils/page';
+import { closeAllCollections, createCollection, createUntitledRequest } from '../../utils/page';
test.describe('Cross-Collection Drag and Drop', () => {
test.afterEach(async ({ page }) => {
@@ -11,14 +11,15 @@ test.describe('Cross-Collection Drag and Drop', () => {
// Create first collection - open with sandbox mode
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
- // 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://echo.usebruno.com');
- await page.getByRole('button', { name: 'Create' }).click();
+ // Create a request in the first collection using the new dropdown flow
+ await createUntitledRequest(page, { requestType: 'HTTP' });
- await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();
+ // Set the URL
+ await page.locator('#request-url .CodeMirror').click();
+ await page.locator('textarea').fill('https://echo.usebruno.com');
+ await page.locator('#send-request').getByTitle('Save Request').click();
+
+ 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' });
@@ -27,7 +28,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
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' });
+ const sourceRequest = page.locator('.item-name').filter({ hasText: /^Untitled/ }).first();
await expect(sourceRequest).toBeVisible();
// Locate the target collection area (the collection name element)
@@ -47,7 +48,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
- targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' })
+ targetCollectionContainer.locator('.item-name').filter({ hasText: /^Untitled/ })
).toBeVisible();
// Verify the request is no longer in the source collection
@@ -56,7 +57,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
- sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' })
+ sourceCollectionContainer.locator('.item-name').filter({ hasText: /^Untitled/ })
).not.toBeVisible();
});
@@ -67,29 +68,31 @@ test.describe('Cross-Collection Drag and Drop', () => {
// Create first collection (source-collection)
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
- // 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://echo.usebruno.com');
- await page.getByRole('button', { name: 'Create' }).click();
+ // Create a request in the first collection using the new dropdown flow
+ await createUntitledRequest(page, { requestType: 'HTTP' });
- // check if request-1 is created and visible in sidebar
- await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible();
+ // Set the URL
+ await page.locator('#request-url .CodeMirror').click();
+ await page.locator('textarea').fill('https://echo.usebruno.com');
+ await page.locator('#send-request').getByTitle('Save Request').click();
+
+ // check if untitled request is created and visible in sidebar
+ 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' });
- // 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();
+ // Create a request in the target collection using the new dropdown flow
+ await createUntitledRequest(page, { requestType: 'HTTP' });
+
+ // Set the URL
+ await page.locator('#request-url .CodeMirror').click();
await page.locator('textarea').fill('https://echo.usebruno.com');
- await page.getByRole('button', { name: 'Create' }).click();
+ 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('.collection-item-name').filter({ hasText: 'request-1' }).first();
+ const sourceRequest = page.locator('.item-name').filter({ hasText: /^Untitled/ }).first();
await expect(sourceRequest).toBeVisible();
// Locate the target collection area
@@ -108,7 +111,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
.filter({ hasText: 'target-collection' })
.locator('..');
await expect(
- targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' })
+ targetCollectionContainer.locator('.item-name').filter({ hasText: /^Untitled/ })
).toBeVisible();
const sourceCollectionContainer = page
@@ -116,7 +119,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
.filter({ hasText: 'source-collection' })
.locator('..');
await expect(
- sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' })
+ sourceCollectionContainer.locator('.item-name').filter({ hasText: /^Untitled/ })
).toBeVisible();
});
});
diff --git a/tests/collection/moving-requests/tag-persistence.spec.ts b/tests/collection/moving-requests/tag-persistence.spec.ts
index 920e20cc0..cb1e52f15 100644
--- a/tests/collection/moving-requests/tag-persistence.spec.ts
+++ b/tests/collection/moving-requests/tag-persistence.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
-import { closeAllCollections } from '../../utils/page';
+import { closeAllCollections, createUntitledRequest } from '../../utils/page';
test.describe('Tag persistence', () => {
test.afterEach(async ({ page }) => {
@@ -20,57 +20,48 @@ test.describe('Tag persistence', () => {
await page.locator('#sidebar-collection-name').filter({ hasText: '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
+ // before returning, ensuring unique names are generated
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'https://httpfaker.org/api/echo',
+ tag: 'smoke'
+ });
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'https://httpfaker.org/api/echo',
+ tag: 'smoke'
+ });
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'https://httpfaker.org/api/echo',
+ tag: 'smoke'
+ });
- // Create a new request
- await page.locator('#create-new-tab').getByRole('img').click();
- await page.getByRole('textbox', { name: 'Request Name' }).fill('request-1');
- await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
- await page.getByRole('button', { name: 'Create' }).click();
+ // Wait for all 3 requests to be visible in the sidebar
+ const untitledRequests = page.locator('.item-name').filter({ hasText: /^Untitled/ });
+ await expect(untitledRequests).toHaveCount(3);
- // create another request
- await page.locator('#create-new-tab').getByRole('img').click();
- await page.getByRole('textbox', { name: 'Request Name' }).fill('request-2');
- 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('request-3');
- 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.waitForTimeout(200);
- const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
- await tagInput.fill('smoke');
- await tagInput.press('Enter');
- await page.waitForTimeout(200);
- // Verify the tag was added
- await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
- await page.keyboard.press('Meta+s');
-
- // Move the request-3 request to just above request-1 within the same collection
- const r3Request = page.locator('.collection-item-name').filter({ hasText: 'request-3' });
- const r1Request = page.locator('.collection-item-name').filter({ hasText: 'request-1' });
+ // Move the last untitled request to just above the first untitled request within the same collection
+ const r3Request = untitledRequests.nth(2); // Third request (0-indexed)
+ const r1Request = untitledRequests.first(); // First request
await expect(r3Request).toBeVisible();
await expect(r1Request).toBeVisible();
- // Perform drag and drop operation to move request-3 below request-1 using source position
+ // Perform drag and drop operation to move the last request above the first using source position
await r3Request.dragTo(r1Request, {
targetPosition: { x: 0, y: 1 }
});
- // Verify the requests are still in the collection and request-3 is now above request-1
- await expect(page.locator('.collection-item-name').filter({ hasText: 'request-3' })).toBeVisible();
- await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible();
+ // Verify the requests are still in the collection
+ await expect(untitledRequests).toHaveCount(3);
- // Click on request-3 to verify the tag persisted after the move
- await page.locator('.collection-item-name').filter({ hasText: 'request-3' }).click();
- await page.locator('.request-tab.active').filter({ hasText: 'request-3' }).waitFor({ state: 'visible' });
+ // Click on the moved request (now first) to verify the tag persisted after the move
+ await untitledRequests.first().click();
+ await page.locator('.request-tab.active').waitFor({ state: 'visible' });
await page.getByRole('tab', { name: 'Settings' }).click();
await page.waitForTimeout(200);
// Verify the tag is still present after the move
diff --git a/tests/request/encoding/curl-encoding.spec.ts b/tests/request/encoding/curl-encoding.spec.ts
index b7b05f906..0bb8c47fb 100644
--- a/tests/request/encoding/curl-encoding.spec.ts
+++ b/tests/request/encoding/curl-encoding.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
-import { closeAllCollections } from '../../utils/page';
+import { closeAllCollections, createUntitledRequest } from '../../utils/page';
test.describe('Code Generation URL Encoding', () => {
test.afterEach(async ({ page }) => {
@@ -33,15 +33,14 @@ test.describe('Code Generation URL Encoding', () => {
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
- await page.locator('#create-new-tab').getByRole('img').click();
- await page.getByPlaceholder('Request Name').fill('unencoded-request');
- await page.locator('#new-request-url .CodeMirror').click();
- await page.locator('textarea').fill('http://base.source?name=John Doe');
- await page.getByRole('button', { name: 'Create' }).click();
+ // Create a new request using the new dropdown flow
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'http://base.source?name=John Doe'
+ });
- await expect(page.locator('.collection-item-name').filter({ hasText: 'unencoded-request' })).toBeVisible();
-
- await page.locator('.collection-item-name').filter({ hasText: 'unencoded-request' }).click();
+ // Find the untitled request and click on it
+ await page.locator('.item-name').filter({ hasText: /^Untitled/ }).first().click();
await page.locator('#send-request .infotip').first().click();
@@ -79,15 +78,14 @@ test.describe('Code Generation URL Encoding', () => {
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
- await page.locator('#create-new-tab').getByRole('img').click();
- await page.getByPlaceholder('Request Name').fill('encoded-request');
- await page.locator('#new-request-url .CodeMirror').click();
- await page.locator('textarea').fill('http://base.source?name=John%20Doe');
- await page.getByRole('button', { name: 'Create' }).click();
+ // Create a new request using the new dropdown flow
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'http://base.source?name=John%20Doe'
+ });
- await expect(page.locator('.collection-item-name').filter({ hasText: 'encoded-request' })).toBeVisible();
-
- await page.locator('.collection-item-name').filter({ hasText: 'encoded-request' }).click();
+ // Find the untitled request and click on it
+ await page.locator('.item-name').filter({ hasText: /^Untitled/ }).first().click();
await page.locator('#send-request .infotip').first().click();
diff --git a/tests/response/large-response-crash-prevention.spec.ts b/tests/response/large-response-crash-prevention.spec.ts
index c638365d9..39df5105c 100644
--- a/tests/response/large-response-crash-prevention.spec.ts
+++ b/tests/response/large-response-crash-prevention.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from '../../playwright';
-import { closeAllCollections, createCollection } from '../utils/page/actions';
+import { closeAllCollections, createCollection, createUntitledRequest } from '../utils/page/actions';
test.describe('Large Response Crash/High Memory Usage Prevention', () => {
// Increase timeout to 1 minute for all tests in this describe block, default is 30 seconds.
@@ -17,14 +17,11 @@ test.describe('Large Response Crash/High Memory Usage Prevention', () => {
// Create collection
await createCollection(page, collectionName, await createTmpDir(collectionName), { openWithSandboxMode: 'safe' });
- // Create request
- await page.locator('#create-new-tab').getByRole('img').click();
-
- const createRequestModal = page.locator('.bruno-modal-card').filter({ hasText: 'New Request' });
- await createRequestModal.getByPlaceholder('Request Name').fill('size-check');
- await createRequestModal.locator('#new-request-url .CodeMirror').click();
- await createRequestModal.locator('textarea').fill('https://samples.json-format.com/employees/json/employees_50MB.json');
- await createRequestModal.getByRole('button', { name: 'Create' }).click();
+ // Create request using the new dropdown flow
+ await createUntitledRequest(page, {
+ requestType: 'HTTP',
+ url: 'https://samples.json-format.com/employees/json/employees_50MB.json'
+ });
// Send request
const sendButton = page.getByTestId('send-arrow-icon');
diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts
index 720905bd1..3efabdf6f 100644
--- a/tests/utils/page/actions.ts
+++ b/tests/utils/page/actions.ts
@@ -89,6 +89,68 @@ type CreateRequestOptions = {
inFolder?: boolean;
};
+type CreateUntitledRequestOptions = {
+ requestType?: 'HTTP' | 'GraphQL' | 'WebSocket' | 'gRPC';
+ requestName?: string;
+ url?: string;
+ tag?: string;
+};
+
+/**
+ * Create an untitled request using the new dropdown flow (from tabs area)
+ * @param page - The page object
+ * @param options - Optional settings (requestType, url, tag)
+ * @returns void
+ */
+const createUntitledRequest = async (
+ page: Page,
+ options: CreateUntitledRequestOptions = {}
+) => {
+ const { requestType = 'HTTP', url, tag } = options;
+
+ await test.step(`Create untitled ${requestType} request${url ? ' with URL' : ''}${tag ? ' with tag' : ''}`, async () => {
+ // Click the + icon to open the dropdown
+ const createButton = page.locator('.short-tab').locator('svg').first();
+ await createButton.waitFor({ state: 'visible' });
+ await createButton.click();
+
+ // Select the request type from dropdown
+ await page.locator('.tippy-box .dropdown-item').filter({ hasText: requestType }).waitFor({ state: 'visible' });
+ await page.locator('.tippy-box .dropdown-item').filter({ hasText: requestType }).click();
+
+ // Wait for the request tab to be active
+ await page.locator('.request-tab.active').waitFor({ state: 'visible' });
+ await page.waitForTimeout(300);
+
+ // Fill URL if provided
+ if (url) {
+ await page.locator('#request-url .CodeMirror').click();
+ await page.locator('#request-url textarea').fill(url);
+ await page.locator('#send-request').getByTitle('Save Request').click();
+ await page.waitForTimeout(200);
+ }
+
+ // Add tag if provided
+ if (tag) {
+ await page.getByRole('tab', { name: 'Settings' }).click();
+ await page.waitForTimeout(200);
+ const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
+ await tagInput.fill(tag);
+ await tagInput.press('Enter');
+ await page.waitForTimeout(200);
+ await expect(page.locator('.tag-item', { hasText: tag })).toBeVisible();
+ await page.keyboard.press('Meta+s');
+ await page.waitForTimeout(200);
+ }
+
+ // Wait for toast message to ensure request creation is complete
+ // This helps prevent race conditions when creating multiple requests
+ await expect(page.getByText('New request created!')).toBeVisible({ timeout: 10000 }).catch(() => {
+ // Toast might have already disappeared, that's okay
+ });
+ });
+};
+
/**
* Create a request in a collection or folder
* @param page - The page object
@@ -521,6 +583,7 @@ export {
openCollectionAndAcceptSandbox,
createCollection,
createRequest,
+ createUntitledRequest,
deleteRequest,
importCollection,
removeCollection,
@@ -539,4 +602,4 @@ export {
expectResponseContains
};
-export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions };
+export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };