diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js index 5e3e98491..af201545b 100644 --- a/packages/bruno-app/src/globalStyles.js +++ b/packages/bruno-app/src/globalStyles.js @@ -294,6 +294,11 @@ const GlobalStyle = createGlobalStyle` font-size: ${(props) => props.theme.font.size.base}; color: ${(props) => props.theme.codemirror.variable.info.color}; font-weight: 500; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Scope Badge */ @@ -305,6 +310,7 @@ const GlobalStyle = createGlobalStyle` font-size: ${(props) => props.theme.font.size.sm}; color: #D97706; letter-spacing: 0.03125rem; + flex-shrink: 0; } /* Value Container */ diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index 0559500de..d26a1eb5e 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -6,7 +6,7 @@ * LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3 */ -import { interpolate } from '@usebruno/common'; +import { interpolate, mockDataFunctions } from '@usebruno/common'; import { getVariableScope, isVariableSecret, getAllVariables } from 'utils/collections'; import { updateVariableInScope } from 'providers/ReduxStore/slices/collections/actions'; import store from 'providers/ReduxStore'; @@ -73,6 +73,8 @@ const getScopeLabel = (scopeType) => { 'request': 'Request', 'runtime': 'Runtime', 'process.env': 'Process Env', + 'dynamic': 'Dynamic', + 'oauth2': 'OAuth2', 'undefined': 'Undefined' }; return labels[scopeType] || scopeType; @@ -178,9 +180,28 @@ export const renderVarInfo = (token, options) => { const collection = options.collection; const item = options.item; - // Check if this is a process.env variable (starts with "process.env.") + // Check if this is a dynamic/faker variable (starts with "$") let scopeInfo; - if (variableName.startsWith('process.env.')) { + if (variableName.startsWith('$oauth2.')) { + // OAuth2 token variable - look up in variables object + const oauth2Value = get(options.variables, variableName); + scopeInfo = { + type: 'oauth2', + value: oauth2Value !== undefined ? oauth2Value : '', + data: null, + isValidOAuth2Variable: oauth2Value !== undefined + }; + } else if (variableName.startsWith('$')) { + const fakerKeyword = variableName.substring(1); // Remove the $ prefix + const fakerFunction = mockDataFunctions[fakerKeyword]; + scopeInfo = { + type: 'dynamic', + value: '', + data: null, + isValidFakerVariable: !!fakerFunction + }; + } else if (variableName.startsWith('process.env.')) { + // Check if this is a process.env variable (starts with "process.env.") scopeInfo = { type: 'process.env', value: variableValue || '', @@ -229,8 +250,8 @@ export const renderVarInfo = (token, options) => { } } - // Check if variable is read-only (process.env, runtime, and undefined variables cannot be edited) - const isReadOnly = scopeInfo.type === 'process.env' || scopeInfo.type === 'runtime' || scopeInfo.type === 'undefined'; + // Check if variable is read-only (process.env, runtime, dynamic/faker, oauth2, and undefined variables cannot be edited) + const isReadOnly = scopeInfo.type === 'process.env' || scopeInfo.type === 'runtime' || scopeInfo.type === 'dynamic' || scopeInfo.type === 'oauth2' || scopeInfo.type === 'undefined'; // Get raw value from scope const rawValue = scopeInfo.value || ''; @@ -265,8 +286,8 @@ export const renderVarInfo = (token, options) => { header.appendChild(scopeBadge); into.appendChild(header); - // Check if variable name is valid (only for non-process.env variables) - const isValidVariableName = scopeInfo.type === 'process.env' || variableNameRegex.test(variableName); + // Check if variable name is valid + const isValidVariableName = scopeInfo.type === 'process.env' || scopeInfo.type === 'dynamic' || scopeInfo.type === 'oauth2' || variableNameRegex.test(variableName); // Show warning if variable name is invalid if (!isValidVariableName) { @@ -279,6 +300,33 @@ export const renderVarInfo = (token, options) => { return into; } + // Show warning for invalid faker variable (starts with $ but not a valid faker function) + if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidFakerVariable) { + const warningNote = document.createElement('div'); + warningNote.className = 'var-warning-note'; + warningNote.textContent = `Unknown dynamic variable "${variableName}". Check the variable name.`; + into.appendChild(warningNote); + return into; + } + + // For valid dynamic variables, just show the read-only note (no value display since it's generated at runtime) + if (scopeInfo.type === 'dynamic' && scopeInfo.isValidFakerVariable) { + const readOnlyNote = document.createElement('div'); + readOnlyNote.className = 'var-readonly-note'; + readOnlyNote.textContent = 'Generates random value on each request'; + into.appendChild(readOnlyNote); + return into; + } + + // Show warning for invalid OAuth2 variable (token not found) + if (scopeInfo.type === 'oauth2' && !scopeInfo.isValidOAuth2Variable) { + const warningNote = document.createElement('div'); + warningNote.className = 'var-warning-note'; + warningNote.textContent = `OAuth2 token not found. Make sure you have fetched the token with the correct Token ID.`; + into.appendChild(warningNote); + return into; + } + // Value container with icons const valueContainer = document.createElement('div'); valueContainer.className = 'var-value-container'; @@ -531,6 +579,11 @@ export const renderVarInfo = (token, options) => { readOnlyNote.className = 'var-readonly-note'; readOnlyNote.textContent = 'Set by scripts (read-only)'; into.appendChild(readOnlyNote); + } else if (scopeInfo.type === 'oauth2') { + const readOnlyNote = document.createElement('div'); + readOnlyNote.className = 'var-readonly-note'; + readOnlyNote.textContent = 'read-only'; + into.appendChild(readOnlyNote); } else if (scopeInfo.type === 'undefined') { const readOnlyNote = document.createElement('div'); readOnlyNote.className = 'var-readonly-note'; diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js index 42e565cf7..5595b826d 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js @@ -3,7 +3,13 @@ import { COPY_SUCCESS_TIMEOUT, extractVariableInfo, renderVarInfo } from './brun // Mock the dependencies jest.mock('@usebruno/common', () => ({ - interpolate: jest.fn() + interpolate: jest.fn(), + mockDataFunctions: { + randomFirstName: jest.fn(() => 'John'), + randomLastName: jest.fn(() => 'Doe'), + randomEmail: jest.fn(() => 'john.doe@example.com'), + randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000') + } })); jest.mock('providers/ReduxStore', () => ({ @@ -424,4 +430,93 @@ describe('renderVarInfo', () => { expect(console.error).toHaveBeenCalledWith('Failed to copy to clipboard:', 'Clipboard error'); }); }); + + describe('dynamic/faker variable rendering', () => { + function setupDynamicRender(variableName, variables = {}) { + const result = renderVarInfo({ string: `{{${variableName}}}` }, { variables, collection: null, item: null }); + if (!result) return { result: null, containerDiv: null }; + + const containerDiv = result; + const header = containerDiv.querySelector('.var-info-header'); + const scopeBadge = containerDiv.querySelector('.var-scope-badge'); + const readOnlyNote = containerDiv.querySelector('.var-readonly-note'); + const warningNote = containerDiv.querySelector('.var-warning-note'); + const valueContainer = containerDiv.querySelector('.var-value-container'); + + return { result, containerDiv, header, scopeBadge, readOnlyNote, warningNote, valueContainer }; + } + + it('should show read-only note for dynamic variables', () => { + const { readOnlyNote } = setupDynamicRender('$randomFirstName'); + + expect(readOnlyNote).not.toBeNull(); + expect(readOnlyNote.textContent).toBe('Generates random value on each request'); + }); + + it('should not show value container for valid dynamic variables', () => { + const { valueContainer } = setupDynamicRender('$randomFirstName'); + + // Value is generated at runtime, so no value display + expect(valueContainer).toBeNull(); + }); + + it('should show warning for unknown dynamic variable', () => { + const { warningNote, scopeBadge } = setupDynamicRender('$unknownFaker'); + + expect(scopeBadge.textContent).toBe('Dynamic'); + expect(warningNote).not.toBeNull(); + expect(warningNote.textContent).toContain('Unknown dynamic variable'); + }); + }); + + describe('OAuth2 variable rendering', () => { + function setupOAuth2Render(variableName, variables = {}) { + const result = renderVarInfo({ string: `{{${variableName}}}` }, { variables, collection: null, item: null }); + if (!result) return { result: null, containerDiv: null }; + + const containerDiv = result; + const header = containerDiv.querySelector('.var-info-header'); + const scopeBadge = containerDiv.querySelector('.var-scope-badge'); + const readOnlyNote = containerDiv.querySelector('.var-readonly-note'); + const warningNote = containerDiv.querySelector('.var-warning-note'); + const valueContainer = containerDiv.querySelector('.var-value-container'); + const valueDisplay = containerDiv.querySelector('.var-value-display'); + + return { result, containerDiv, header, scopeBadge, readOnlyNote, warningNote, valueContainer, valueDisplay }; + } + + it('should show OAuth2 scope badge for $oauth2 variables', () => { + const { scopeBadge } = setupOAuth2Render('$oauth2.credentials.access_token', { + '$oauth2.credentials.access_token': 'test-token-123' + }); + + expect(scopeBadge.textContent).toBe('OAuth2'); + }); + + it('should show read-only note for valid OAuth2 variables', () => { + const { readOnlyNote } = setupOAuth2Render('$oauth2.credentials.access_token', { + '$oauth2.credentials.access_token': 'test-token-123' + }); + + expect(readOnlyNote).not.toBeNull(); + expect(readOnlyNote.textContent).toBe('read-only'); + }); + + it('should display the token value for valid OAuth2 variables', () => { + const { valueDisplay } = setupOAuth2Render('$oauth2.credentials.access_token', { + '$oauth2.credentials.access_token': 'test-token-123' + }); + + expect(valueDisplay).not.toBeNull(); + expect(valueDisplay.textContent).toBe('test-token-123'); + }); + + it('should show warning for OAuth2 variable when token is not found', () => { + const { warningNote, scopeBadge } = setupOAuth2Render('$oauth2.credentials.access_token', {}); + + expect(scopeBadge.textContent).toBe('OAuth2'); + expect(warningNote).not.toBeNull(); + expect(warningNote.textContent).toContain('OAuth2 token not found'); + }); + }); });