diff --git a/packages/bruno-api-docs/src/components/VariableText/VariableText.spec.tsx b/packages/bruno-api-docs/src/components/VariableText/VariableText.spec.tsx index 8c815ec5..a4ce6971 100644 --- a/packages/bruno-api-docs/src/components/VariableText/VariableText.spec.tsx +++ b/packages/bruno-api-docs/src/components/VariableText/VariableText.spec.tsx @@ -9,7 +9,18 @@ import { useRenderToDom } from '@/hooks/useRenderToDom'; import { VariableText } from './VariableText'; const collection: any = { - config: { environments: [{ name: 'Dev', variables: [{ name: 'baseUrl', value: 'https://dev.test' }] }] } + config: { + environments: [ + { + name: 'Dev', + variables: [ + { name: 'baseUrl', value: 'https://dev.test' }, + { name: 'emptyValue', value: '' }, + { name: 'authToken', secret: true } + ] + } + ] + } }; const tree = (value: string, configure?: (store: ReturnType) => void) => { @@ -58,10 +69,24 @@ describe('VariableText', () => { expect(root.querySelector('.var-text')?.text).toBe('https://dev.test/api/v1/auth'); }); - it('still highlights an unknown variable when show-variables is on', () => { + it('shows the (empty) placeholder for a defined variable that has no value', () => { + const root = useRenderToDom(tree('{{emptyValue}}', withShowVars)); + const token = root.querySelector('.var'); + expect(token?.getAttribute('data-var-name')).toBe('emptyValue'); + expect(token?.text).toBe('(empty)'); + expect(token?.getAttribute('class')).toContain('var-empty'); + }); + + it('shows the (empty) placeholder for a variable that is not defined at all', () => { const root = useRenderToDom(tree('{{unknown}}/x', withShowVars)); const token = root.querySelector('.var'); expect(token?.getAttribute('data-var-name')).toBe('unknown'); - expect(token?.text).toBe('{{unknown}}'); + expect(token?.text).toBe('(empty)'); + }); + it('leaves a secret and a dynamic reference untouched when show-variables is on', () => { + expect(useRenderToDom(tree('{{authToken}}', withShowVars)).querySelector('.var')?.text) + .toBe('{{authToken}}'); + expect(useRenderToDom(tree('{{$randomInt}}', withShowVars)).querySelector('.var')?.text) + .toBe('{{$randomInt}}'); }); }); diff --git a/packages/bruno-api-docs/src/components/VariableText/VariableToken/StyledWrapper.ts b/packages/bruno-api-docs/src/components/VariableText/VariableToken/StyledWrapper.ts index 828f3d1c..3b5b1bc2 100644 --- a/packages/bruno-api-docs/src/components/VariableText/VariableToken/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/VariableText/VariableToken/StyledWrapper.ts @@ -11,4 +11,9 @@ export const StyledWrapper = styled.span` &.var-plain { cursor: pointer; } + + &.var-empty { + font-style: italic; + color: var(--text-tertiary); + } `; diff --git a/packages/bruno-api-docs/src/components/VariableText/VariableToken/VariableToken.tsx b/packages/bruno-api-docs/src/components/VariableText/VariableToken/VariableToken.tsx index 6f00f41d..a61620ea 100644 --- a/packages/bruno-api-docs/src/components/VariableText/VariableToken/VariableToken.tsx +++ b/packages/bruno-api-docs/src/components/VariableText/VariableToken/VariableToken.tsx @@ -1,20 +1,32 @@ import React from 'react'; import { useResolvedVariables } from '@/hooks'; import { Popover } from '@/ui/Popover/Popover'; +import { detectSpecialScope, isValidVariableName } from '@/utils/variableResolution'; import { VariableInfoCard } from '../../VariableInfoCard/VariableInfoCard'; import { StyledWrapper } from './StyledWrapper'; +const EMPTY_PLACEHOLDER = '(empty)'; + export const VariableToken: React.FC<{ token: string; highlighted?: boolean }> = ({ token, highlighted = true }) => { - const { showVars, resolve } = useResolvedVariables(); + const { showVars, resolve, isFound, isSecret } = useResolvedVariables(); const name = token.slice(2, -2).trim(); - const display = showVars ? resolve(token) : token; - const revealed = display !== token; + const resolved = showVars ? resolve(token) : token; + const revealed = resolved !== token; + + const plainVariable = isValidVariableName(name) && !isSecret(name) && !detectSpecialScope(name); + const empty = showVars && plainVariable && (resolved === '' || !isFound(name)); + + const display = empty ? EMPTY_PLACEHOLDER : resolved; const variant = highlighted ? 'var-highlight' : revealed ? undefined : 'var-plain'; return ( - } testId="variable-info-popover" disabled={revealed}> + } + testId="variable-info-popover" + disabled={revealed || empty} + > { expect(html).toContain('undefined'); }); }); + +const NestedProbe: React.FC = () => { + const r = useResolvedVariables(); + return ( +
+ {r.resolve('{{endpoint}}')} + {r.interpolate('{{endpoint}}')} +
+ ); +}; + +describe('nested variable resolution', () => { + const nested = { + request: { + variables: [ + { name: 'endpoint', value: '{{host}}/v1' }, + { name: 'host', value: 'https://api.test' } + ] + }, + config: { environments: [{ name: 'Dev', variables: [] }] } + } as unknown as typeof collection; + + it('follows a variable that points at another variable, for display and for interpolation', () => { + const store = createOpenCollectionStore(); + store.dispatch(setDocsCollection(nested)); + store.dispatch(setActiveEnv('Dev')); + store.dispatch(setShowVars(true)); + + const html = renderToStaticMarkup( + + + + + + ); + + expect(html).toContain('https://api.test/v1'); + expect(html).toContain('https://api.test/v1'); + }); +}); diff --git a/packages/bruno-api-docs/src/hooks/useVariableResolver.tsx b/packages/bruno-api-docs/src/hooks/useVariableResolver.tsx index e88b2224..f07b9d64 100644 --- a/packages/bruno-api-docs/src/hooks/useVariableResolver.tsx +++ b/packages/bruno-api-docs/src/hooks/useVariableResolver.tsx @@ -12,7 +12,7 @@ import { getItemUuid } from '@/utils/itemUtils'; import { mockDataFunctions, timeBasedDynamicVars } from '@/runner/utils/faker-functions'; import { buildScopedVariableModel, - resolveVariables, + resolveValueDeep, singleReferenceName, detectSpecialScope, isValidVariableName, @@ -95,7 +95,7 @@ const makeResolver = ( activeEnvName: string | null ): VariableResolver => { const isSecret = (name: string) => model.secretNames.has(name.trim()); - const interpolate = (raw: string) => resolveVariables(raw, model.values); + const interpolate = (raw: string) => resolveValueDeep(raw, model.values); return { showVars, activeEnvName, @@ -263,7 +263,7 @@ export const ItemVariableResolverProvider: React.FC<{ ); const interpolateWithSecrets = useCallback( - (raw: string) => resolveVariables(raw, model.fullValues), + (raw: string) => resolveValueDeep(raw, model.fullValues), [model] );