mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-28 07:04:10 +00:00
Feat/editor custom search (#5278)
* added custom search in editor * UI improvements * added yellow highlight for search * added playwright tests * memoizing matches and few other changes * fixed issue with debounce * refactoring and styling fixes * lint fixes * ensure ESC closes search bar even when focus is in editor * move esc logic to editor --------- Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 2px;
|
||||
min-height: 36px;
|
||||
background: ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
min-width: 80px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1px 2px;
|
||||
font-size: 13px;
|
||||
margin: 0 1px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchbar-result-count {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
margin: 0 8px 0 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bruno-search-bar.compact {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.codemirror.text || props.theme.text};
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
padding: 1px 2px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn:focus {
|
||||
outline: 1px solid ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.bruno-search-bar, .bruno-search-bar input {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,202 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
const CustomSearch = ({ visible, editor, onClose }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const [wholeWord, setWholeWord] = useState(false);
|
||||
const [matchIndex, setMatchIndex] = useState(0);
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
const memoizedMatches = useMemo(() => {
|
||||
if (!editor || !visible) return [];
|
||||
if (!debouncedSearchText) return [];
|
||||
|
||||
try {
|
||||
let query, options = {};
|
||||
if (regex) {
|
||||
try {
|
||||
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
} else if (wholeWord) {
|
||||
const escaped = escapeRegExp(debouncedSearchText);
|
||||
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
|
||||
} else {
|
||||
query = debouncedSearchText;
|
||||
options = { caseFold: !caseSensitive };
|
||||
}
|
||||
|
||||
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
|
||||
const out = [];
|
||||
while (cursor.findNext()) {
|
||||
out.push({ from: cursor.from(), to: cursor.to() });
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
return [];
|
||||
}
|
||||
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
|
||||
|
||||
const doSearch = useCallback((newIndex = 0) => {
|
||||
if (!editor) return;
|
||||
|
||||
// Clear previous marks
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
// Clear previous line highlight
|
||||
if (searchLineHighlight.current !== null) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
if (!debouncedSearchText) {
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = memoizedMatches;
|
||||
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
|
||||
matches.forEach((m, i) => {
|
||||
const mark = editor.markText(m.from, m.to, {
|
||||
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
|
||||
clearOnEnter: true
|
||||
});
|
||||
searchMarks.current.push(mark);
|
||||
});
|
||||
|
||||
if (matches.length) {
|
||||
const currentLine = matches[matchIndex].from.line;
|
||||
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = currentLine;
|
||||
|
||||
editor.scrollIntoView(matches[matchIndex].from, 100);
|
||||
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
|
||||
} else {
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
setMatchCount(matches.length);
|
||||
setMatchIndex(matchIndex);
|
||||
searchMatches.current = matches;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
|
||||
const handleSearchBarClose = useCallback(() => {
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
if (searchLineHighlight.current !== null && editor) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
searchMatches.current = [];
|
||||
if (onClose) onClose();
|
||||
// Focus the editor after closing the search bar
|
||||
if (editor) {
|
||||
setTimeout(() => editor.focus(), 0);
|
||||
}
|
||||
}, [editor, onClose]);
|
||||
|
||||
const handleSearchTextChange = (text) => {
|
||||
setSearchText(text);
|
||||
setMatchIndex(0);
|
||||
};
|
||||
|
||||
const handleToggleRegex = () => {
|
||||
setRegex((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleCase = () => {
|
||||
setCaseSensitive((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleWholeWord = () => {
|
||||
setWholeWord((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let next = (matchIndex + 1) % searchMatches.current.length;
|
||||
setMatchIndex(next);
|
||||
doSearch(next);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
|
||||
setMatchIndex(prev);
|
||||
doSearch(prev);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar compact">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchTextChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) handleNext();
|
||||
if (e.key === 'Enter' && e.shiftKey) handlePrev();
|
||||
if (e.key === 'Escape') handleSearchBarClose();
|
||||
}}
|
||||
/>
|
||||
<span className="searchbar-result-count">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>
|
||||
<ToolHint text="Regex search" toolhintId="searchbar-regex-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Case sensitive" toolhintId="searchbar-case-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Whole word" toolhintId="searchbar-wholeword-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>
|
||||
</ToolHint>
|
||||
<button className="searchbar-icon-btn" title="Previous" onClick={handlePrev}><IconArrowUp size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Next" onClick={handleNext}><IconArrowDown size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Close" onClick={handleSearchBarClose}><IconX size={14} /></button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomSearch;
|
||||
@@ -109,6 +109,17 @@ const StyledWrapper = styled.div`
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.cm-search-match {
|
||||
background: rgba(255, 193, 7, 0.25);
|
||||
}
|
||||
|
||||
.cm-search-current {
|
||||
background: rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import CustomSearch from './CustomSearch';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -37,6 +38,10 @@ export default class CodeEditor extends React.Component {
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
|
||||
this.state = {
|
||||
searchBarVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -83,24 +88,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
@@ -129,6 +124,11 @@ export default class CodeEditor extends React.Component {
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Esc': () => {
|
||||
if (this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
@@ -254,11 +254,6 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -271,10 +266,18 @@ export default class CodeEditor extends React.Component {
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<CustomSearch
|
||||
visible={this.state.searchBarVisible}
|
||||
editor={this.editor}
|
||||
onClose={() => this.setState({ searchBarVisible: false })}
|
||||
/>
|
||||
<div
|
||||
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
|
||||
ref={(node) => { this._node = node; }}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
19
packages/bruno-app/src/hooks/useDebounce/index.js
Normal file
19
packages/bruno-app/src/hooks/useDebounce/index.js
Normal file
@@ -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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
6
tests/request/collections/custom-search/bruno.json
Normal file
6
tests/request/collections/custom-search/bruno.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "custom-search",
|
||||
"type": "collection",
|
||||
"ignore": ["node_modules", ".git"]
|
||||
}
|
||||
9
tests/request/collections/custom-search/package.json
Normal file
9
tests/request/collections/custom-search/package.json
Normal file
@@ -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"
|
||||
}
|
||||
46
tests/request/collections/custom-search/search-request.bru
Normal file
46
tests/request/collections/custom-search/search-request.bru
Normal file
@@ -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");
|
||||
}
|
||||
133
tests/request/tests/custom-search/custom-search.spec.ts
Normal file
133
tests/request/tests/custom-search/custom-search.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/request/collections/custom-search",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/tests/request/collections/custom-search"],
|
||||
"preferences": {}
|
||||
}
|
||||
Reference in New Issue
Block a user