From bbf4ad6b9809fae856886c0896ad1e0da5192047 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 20 Jun 2025 16:15:11 +0530 Subject: [PATCH] Enable variable tootlip in json request body (#4885) * Enable variable tootlip in json request body * fix: enhance variable value popover and add test coverage --------- Co-authored-by: Maintainer Bruno --- .../src/components/CodeEditor/index.js | 5 + .../src/utils/codemirror/brunoVarInfo.js | 47 ++-- .../src/utils/codemirror/brunoVarInfo.spec.js | 227 ++++++++++++++++++ 3 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 9be42e29c..e87f8627d 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -144,12 +144,17 @@ export default class CodeEditor extends React.Component { } componentDidMount() { + const variables = getAllVariables(this.props.collection, this.props.item); + const editor = (this.editor = CodeMirror(this._node, { value: this.props.value || '', lineNumbers: true, lineWrapping: true, tabSize: TAB_SIZE, mode: this.props.mode || 'application/ld+json', + brunoVarInfo: { + variables + }, keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index cef99a22d..52614703f 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -16,23 +16,8 @@ if (!SERVER_RENDERED) { CodeMirror = require('codemirror'); const renderVarInfo = (token, options, cm, pos) => { - const str = token.string || ''; - if (!str || !str.length || typeof str !== 'string') { - return; - } - - // str is of format {{variableName}} or :variableName, extract variableName - let variableName; - let variableValue; - - if (str.startsWith('{{')) { - variableName = str.replace('{{', '').replace('}}', '').trim(); - variableValue = interpolate(get(options.variables, variableName), options.variables); - } else if (str.startsWith('/:')) { - variableName = str.replace('/:', '').trim(); - variableValue = - options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined; - } + // Extract variable name and value based on token + const { variableName, variableValue } = extractVariableInfo(token.string, options.variables); if (variableValue === undefined) { return; @@ -41,11 +26,13 @@ if (!SERVER_RENDERED) { const into = document.createElement('div'); const descriptionDiv = document.createElement('div'); descriptionDiv.className = 'info-description'; + if (options?.variables?.maskedEnvVariables?.includes(variableName)) { descriptionDiv.appendChild(document.createTextNode('*****')); } else { descriptionDiv.appendChild(document.createTextNode(variableValue)); } + into.appendChild(descriptionDiv); return into; @@ -202,3 +189,29 @@ if (!SERVER_RENDERED) { CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); } } + +export const extractVariableInfo = (str, variables) => { + let variableName; + let variableValue; + + if (!str || !str.length || typeof str !== 'string') { + return { variableName, variableValue }; + } + + // Regex to match double brace variable syntax: {{variableName}} + const DOUBLE_BRACE_PATTERN = /\{\{([^}]+)\}\}/; + + if (DOUBLE_BRACE_PATTERN.test(str)) { + variableName = str.replace('{{', '').replace('}}', '').trim(); + variableValue = interpolate(get(variables, variableName), variables); + } else if (str.startsWith('/:')) { + variableName = str.replace('/:', '').trim(); + variableValue = variables?.pathParams?.[variableName]; + } else { + // direct variable reference (e.g., for numeric values in JSON mode or plain variable names) + variableName = str; + variableValue = interpolate(get(variables, variableName), variables); + } + + return { variableName, variableValue }; +}; \ No newline at end of file diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js new file mode 100644 index 000000000..5002097c2 --- /dev/null +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js @@ -0,0 +1,227 @@ +import { interpolate } from '@usebruno/common'; +import { extractVariableInfo } from './brunoVarInfo'; + +// Mock the dependencies +jest.mock('@usebruno/common', () => ({ + interpolate: jest.fn() +})); + +describe('extractVariableInfo', () => { + let mockVariables; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup mock variables + mockVariables = { + apiKey: 'test-api-key-123', + baseUrl: 'https://api.example.com', + userId: 12345, + pathParams: { + id: 'user-123', + slug: 'test-post' + } + }; + + // Setup interpolate mock + interpolate.mockImplementation((value, variables) => { + if (typeof value === 'string' && value.includes('{{')) { + return value.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match); + } + return value; + }); + }); + + describe('input validation', () => { + it('should return undefined for null input', () => { + const result = extractVariableInfo(null, mockVariables); + expect(result.variableName).toBeUndefined(); + expect(result.variableValue).toBeUndefined(); + }); + + it('should return undefined for undefined input', () => { + const result = extractVariableInfo(undefined, mockVariables); + expect(result.variableName).toBeUndefined(); + expect(result.variableValue).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + const result = extractVariableInfo('', mockVariables); + expect(result.variableName).toBeUndefined(); + expect(result.variableValue).toBeUndefined(); + }); + + it('should return undefined for non-string input', () => { + const result = extractVariableInfo(123, mockVariables); + expect(result.variableName).toBeUndefined(); + expect(result.variableValue).toBeUndefined(); + }); + + it('should return undefined for object input', () => { + const result = extractVariableInfo({}, mockVariables); + expect(result.variableName).toBeUndefined(); + expect(result.variableValue).toBeUndefined(); + }); + }); + + describe('double brace format ({{variableName}})', () => { + it('should parse double brace variables correctly', () => { + const result = extractVariableInfo('{{apiKey}}', mockVariables); + + expect(result).toEqual({ + variableName: 'apiKey', + variableValue: 'test-api-key-123' + }); + + expect(interpolate).toHaveBeenCalledWith('test-api-key-123', mockVariables); + }); + + it('should handle whitespace in double brace variables', () => { + const result = extractVariableInfo('{{ apiKey }}', mockVariables); + + expect(result).toEqual({ + variableName: 'apiKey', + variableValue: 'test-api-key-123' + }); + }); + + it('should return undefined variableValue for non-existent double brace variable', () => { + const result = extractVariableInfo('{{nonExistent}}', mockVariables); + + expect(result).toEqual({ + variableName: 'nonExistent', + variableValue: undefined + }); + }); + }); + + describe('path parameter format (/:variableName)', () => { + it('should parse path parameter variables correctly', () => { + const result = extractVariableInfo('/:id', mockVariables); + + expect(result).toEqual({ + variableName: 'id', + variableValue: 'user-123' + }); + }); + + it('should return undefined for non-existent path parameter', () => { + const result = extractVariableInfo('/:nonExistent', mockVariables); + + expect(result).toEqual({ + variableName: 'nonExistent', + variableValue: undefined + }); + }); + + it('should handle missing pathParams object', () => { + const variablesWithoutPathParams = { ...mockVariables }; + delete variablesWithoutPathParams.pathParams; + + const result = extractVariableInfo('/:id', variablesWithoutPathParams); + + expect(result).toEqual({ + variableName: 'id', + variableValue: undefined + }); + }); + + it('should handle null pathParams', () => { + const variablesWithNullPathParams = { ...mockVariables, pathParams: null }; + + const result = extractVariableInfo('/:id', variablesWithNullPathParams); + + expect(result).toEqual({ + variableName: 'id', + variableValue: undefined + }); + }); + }); + + describe('direct variable format', () => { + it('should parse direct variable names correctly', () => { + const result = extractVariableInfo('baseUrl', mockVariables); + + expect(result).toEqual({ + variableName: 'baseUrl', + variableValue: 'https://api.example.com' + }); + + expect(interpolate).toHaveBeenCalledWith('https://api.example.com', mockVariables); + }); + + it('should handle numeric variable values', () => { + const result = extractVariableInfo('userId', mockVariables); + + expect(result).toEqual({ + variableName: 'userId', + variableValue: 12345 + }); + }); + + it('should return undefined for non-existent direct variable', () => { + const result = extractVariableInfo('nonExistent', mockVariables); + + expect(result).toEqual({ + variableName: 'nonExistent', + variableValue: undefined + }); + }); + + it('should handle variables with special characters', () => { + mockVariables['special-var_name'] = 'special-var_value'; + + const result = extractVariableInfo('special-var_name', mockVariables); + + expect(result).toEqual({ + variableName: 'special-var_name', + variableValue: 'special-var_value' + }); + }); + }); + + describe('edge cases', () => { + it('should handle empty variables object', () => { + const result = extractVariableInfo('{{apiKey}}', {}); + + expect(result).toEqual({ + variableName: 'apiKey', + variableValue: undefined + }); + }); + + it('should handle null variables object', () => { + const result = extractVariableInfo('{{apiKey}}', null); + + expect(result).toEqual({ + variableName: 'apiKey', + variableValue: undefined + }); + }); + + it('should handle undefined variables object', () => { + const result = extractVariableInfo('{{apiKey}}', undefined); + + expect(result).toEqual({ + variableName: 'apiKey', + variableValue: undefined + }); + }); + }); + + describe('return value structure', () => { + it('should always return an object with variableName and variableValue properties', () => { + const result = extractVariableInfo('{{apiKey}}', mockVariables); + + expect(result).toHaveProperty('variableName'); + expect(result).toHaveProperty('variableValue'); + expect(typeof result.variableName).toBe('string'); + }); + + it('should return variableValue as the interpolated value', () => { + const result = extractVariableInfo('{{apiKey}}', mockVariables); + + expect(result.variableValue).toBe('test-api-key-123'); + }); + }); +});