this.setState({ searchBarVisible: false })}
+ />
+ { this._node = node; }}
+ style={{ height: '100%', width: '100%' }}
+ />
+
);
}
@@ -298,67 +301,4 @@ export default class CodeEditor extends React.Component {
}
}
};
-
- _isSearchOpen = () => {
- return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
- };
-
- /**
- * Bind handler to search input to count number of search results
- */
- _bindSearchHandler = () => {
- const searchInput = document.querySelector('.CodeMirror-search-field');
-
- if (searchInput) {
- searchInput.addEventListener('input', this._countSearchResults);
- }
- };
-
- /**
- * Unbind handler to search input to count number of search results
- */
- _unbindSearchHandler = () => {
- const searchInput = document.querySelector('.CodeMirror-search-field');
-
- if (searchInput) {
- searchInput.removeEventListener('input', this._countSearchResults);
- }
- };
-
- /**
- * Append search results count to search dialog
- */
- _appendSearchResultsCount = () => {
- const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
-
- if (dialog) {
- const searchResultsCount = document.createElement('span');
- searchResultsCount.id = this.searchResultsCountElementId;
- dialog.appendChild(searchResultsCount);
-
- this._countSearchResults();
- }
- };
-
- /**
- * Count search results and update state
- */
- _countSearchResults = () => {
- let count = 0;
-
- const searchInput = document.querySelector('.CodeMirror-search-field');
-
- if (searchInput && searchInput.value.length > 0) {
- // Escape special characters in search input to prevent RegExp crashes. Fixes #3051
- const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
- const matches = this.editor.getValue().match(text);
- count = matches ? matches.length : 0;
- }
-
- const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
-
- if (searchResultsCountElement) {
- searchResultsCountElement.innerText = `${count} results`;
- }
- };
}
diff --git a/packages/bruno-app/src/hooks/useDebounce/index.js b/packages/bruno-app/src/hooks/useDebounce/index.js
new file mode 100644
index 000000000..1b2e5cb9e
--- /dev/null
+++ b/packages/bruno-app/src/hooks/useDebounce/index.js
@@ -0,0 +1,19 @@
+import { useState, useEffect } from 'react';
+
+function useDebounce(value, delay) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebounce;
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index 1b574b405..505ddafd2 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -277,7 +277,10 @@ const darkTheme = {
bg: 'rgb(48,48,49)',
boxShadow: 'rgb(0 0 0 / 36%) 0px 2px 8px'
}
- }
+ },
+ searchLineHighlightCurrent: 'rgba(120,120,120,0.18)',
+ searchMatch: '#FFD700',
+ searchMatchActive: '#FFFF00'
},
table: {
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index 98e23bb0b..9a203aafc 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -278,7 +278,10 @@ const lightTheme = {
bg: 'white',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.45)'
}
- }
+ },
+ searchLineHighlightCurrent: 'rgba(120,120,120,0.10)',
+ searchMatch: '#B8860B',
+ searchMatchActive: '#DAA520'
},
table: {
diff --git a/tests/request/collections/custom-search/bruno.json b/tests/request/collections/custom-search/bruno.json
new file mode 100644
index 000000000..082d7a146
--- /dev/null
+++ b/tests/request/collections/custom-search/bruno.json
@@ -0,0 +1,6 @@
+{
+ "version": "1",
+ "name": "custom-search",
+ "type": "collection",
+ "ignore": ["node_modules", ".git"]
+}
diff --git a/tests/request/collections/custom-search/package.json b/tests/request/collections/custom-search/package.json
new file mode 100644
index 000000000..ef8ea6862
--- /dev/null
+++ b/tests/request/collections/custom-search/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "custom-search",
+ "version": "1.0.0",
+ "description": "A test collection for search functionality",
+ "main": "index.js",
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/tests/request/collections/custom-search/search-request.bru b/tests/request/collections/custom-search/search-request.bru
new file mode 100644
index 000000000..2285feda4
--- /dev/null
+++ b/tests/request/collections/custom-search/search-request.bru
@@ -0,0 +1,46 @@
+meta {
+ name: search-test-request
+ type: http
+ seq: 1
+}
+
+get {
+ url: https://httpbin.org/get
+ body: none
+ auth: inherit
+}
+
+script:pre-request {
+ const testVariable = "hello world";
+ const anotherVariable = "search test";
+ console.log("This is a test log message");
+ const searchableText = "find me";
+
+ const apiKey = "test-api-key-123";
+ const baseUrl = "https://api.example.com";
+ console.log("Pre-request script executed");
+
+ const uniquePreVar = "only in pre-request";
+ const commonVar = "common content";
+
+ const searchableText2 = "find me again";
+ const searchableText3 = "find me third time";
+ console.log("More searchableText instances");
+}
+
+script:post-response {
+ const responseData = "response content";
+ const searchableResponse = "find this too";
+ console.log("Response processed");
+
+ const statusCode = bru.getResponseStatus();
+ const responseTime = bru.getResponseTime();
+ console.log("Response status:", statusCode);
+
+ const uniquePostVar = "only in post-response";
+ const commonVar = "common content";
+
+ const searchableResponse2 = "find this too again";
+ const searchableResponse3 = "find this too third time";
+ console.log("More searchableResponse instances");
+}
diff --git a/tests/request/tests/custom-search/custom-search.spec.ts b/tests/request/tests/custom-search/custom-search.spec.ts
new file mode 100644
index 000000000..d9fdb72a0
--- /dev/null
+++ b/tests/request/tests/custom-search/custom-search.spec.ts
@@ -0,0 +1,133 @@
+import { test, expect } from '../../../../playwright';
+
+test.describe('Custom Search Functionality in Scripts Tab', () => {
+ test('should open search box when Cmd+F or Ctrl+F is pressed in scripts tab', async ({ pageWithUserData: page }) => {
+ await page.getByTitle('custom-search').click();
+
+ await page.getByText('search-test-request').click();
+
+ await page.getByRole('tab', { name: 'Script' }).click();
+
+ await expect(page.getByText('Pre Request')).toBeVisible();
+ await expect(page.locator('.title.text-xs').filter({ hasText: 'Post Response' })).toBeVisible();
+
+ const preRequestEditor = page.locator('text=Pre Request').locator('..').locator('.CodeMirror').first();
+ const preTextarea = preRequestEditor.locator('textarea[tabindex="0"]');
+ await preTextarea.focus();
+
+ const preContent = await preRequestEditor.textContent();
+ console.log('Pre Request content loaded:', preContent?.substring(0, 100));
+
+ const postResponseEditor = page.locator('text=Post Response').locator('..').locator('.CodeMirror').first();
+ const postTextarea = postResponseEditor.locator('textarea[tabindex="0"]');
+ await postTextarea.focus();
+
+ const postContent = await postResponseEditor.textContent();
+ console.log('Post Response content loaded:', postContent?.substring(0, 100));
+
+ await preTextarea.focus();
+ await page.keyboard.press('Meta+f');
+
+ // Verify search box appears
+ await expect(page.locator('.bruno-search-bar input[placeholder="Search..."]')).toBeVisible();
+
+ // Test search functionality
+ const searchInput = page.locator('.bruno-search-bar input[placeholder="Search..."]');
+ await searchInput.fill('searchableText');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 4');
+
+ // Test search options
+ const regexButton = page.locator('.searchbar-icon-btn').filter({ hasText: '' }).first();
+ const caseSensitiveButton = page.locator('.searchbar-icon-btn').filter({ hasText: '' }).nth(1);
+ const wholeWordButton = page.locator('.searchbar-icon-btn').filter({ hasText: '' }).nth(2);
+
+ // Test regex search
+ await regexButton.click();
+ await searchInput.fill('test\\w+');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 1');
+
+ // Test case sensitive search
+ await regexButton.click();
+ await caseSensitiveButton.click();
+ await searchInput.fill('Test');
+ await expect(page.locator('.searchbar-result-count')).toContainText('0 results');
+
+ // Test whole word search
+ await caseSensitiveButton.click();
+ await wholeWordButton.click();
+ await searchInput.fill('hello');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 1');
+
+ // Test close search
+ const closeButton = page.locator('.searchbar-icon-btn').last();
+ await closeButton.click();
+ await expect(page.locator('.bruno-search-bar')).not.toBeVisible();
+ });
+
+ test('should handle search in different script editors independently', async ({ pageWithUserData: page }) => {
+ await page.getByTitle('custom-search').click();
+
+ await page.getByText('search-test-request').click();
+
+ await page.getByRole('tab', { name: 'Script' }).click();
+
+ await expect(page.getByText('Pre Request')).toBeVisible();
+ await expect(page.locator('.title.text-xs').filter({ hasText: 'Post Response' })).toBeVisible();
+
+ const preRequestEditor = page.locator('text=Pre Request').locator('..').locator('.CodeMirror').first();
+ const postResponseEditor = page.locator('text=Post Response').locator('..').locator('.CodeMirror').first();
+
+ const preTextarea = preRequestEditor.locator('textarea[tabindex="0"]');
+ await preTextarea.focus();
+ await page.keyboard.press('Meta+f');
+
+ const preSearchInput = page.locator('.bruno-search-bar input[placeholder="Search..."]');
+ await preSearchInput.fill('uniquePreVar');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 1');
+ await page.keyboard.press('Escape');
+
+ const postTextarea = postResponseEditor.locator('textarea[tabindex="0"]');
+ await postTextarea.focus();
+ await page.keyboard.press('Meta+f');
+
+ const postSearchInput = page.locator('.bruno-search-bar input[placeholder="Search..."]');
+ await postSearchInput.fill('uniquePostVar');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 1');
+ await page.keyboard.press('Escape');
+ });
+
+ test('should maintain search state when switching between editors', async ({ pageWithUserData: page }) => {
+ await page.getByTitle('custom-search').click();
+
+ await page.getByText('search-test-request').click();
+
+ await page.getByRole('tab', { name: 'Script' }).click();
+
+ await expect(page.getByText('Pre Request')).toBeVisible();
+ await expect(page.locator('.title.text-xs').filter({ hasText: 'Post Response' })).toBeVisible();
+
+ const preRequestEditor = page.locator('text=Pre Request').locator('..').locator('.CodeMirror').first();
+ const postResponseEditor = page.locator('text=Post Response').locator('..').locator('.CodeMirror').first();
+
+ // Open search in Pre Request editor
+ const preTextarea = preRequestEditor.locator('textarea[tabindex="0"]');
+ await preTextarea.focus();
+ await page.keyboard.press('Meta+f');
+
+ const searchInput = page.locator('.bruno-search-bar input[placeholder="Search..."]');
+ await searchInput.fill('commonVar');
+ await expect(page.locator('.searchbar-result-count')).toContainText('1 / 1');
+
+ // Switch to Post Response editor while search is open
+ const postTextarea = postResponseEditor.locator('textarea[tabindex="0"]');
+ await postTextarea.focus();
+
+ // Search should still be visible and functional
+ await expect(page.locator('.bruno-search-bar')).toBeVisible();
+ await expect(searchInput).toHaveValue('commonVar');
+
+ const closeButton = page.locator('.searchbar-icon-btn').last();
+ await closeButton.click();
+ await expect(page.locator('.bruno-search-bar')).not.toBeVisible();
+ });
+});
diff --git a/tests/request/tests/custom-search/init-user-data/collection-security.json b/tests/request/tests/custom-search/init-user-data/collection-security.json
new file mode 100644
index 000000000..cd07228fe
--- /dev/null
+++ b/tests/request/tests/custom-search/init-user-data/collection-security.json
@@ -0,0 +1,10 @@
+{
+ "collections": [
+ {
+ "path": "{{projectRoot}}/tests/request/collections/custom-search",
+ "securityConfig": {
+ "jsSandboxMode": "safe"
+ }
+ }
+ ]
+}
diff --git a/tests/request/tests/custom-search/init-user-data/preferences.json b/tests/request/tests/custom-search/init-user-data/preferences.json
new file mode 100644
index 000000000..8a42569a2
--- /dev/null
+++ b/tests/request/tests/custom-search/init-user-data/preferences.json
@@ -0,0 +1,5 @@
+{
+ "maximized": true,
+ "lastOpenedCollections": ["{{projectRoot}}/tests/request/collections/custom-search"],
+ "preferences": {}
+}