From 2fbc878c5a9eee8ac6bf38f3c8967ebb14743229 Mon Sep 17 00:00:00 2001 From: Adwait Aayush Date: Fri, 28 Aug 2026 18:22:03 +0530 Subject: [PATCH] feat:interpolate variables in code snippet modal --- .../request/code-snippet.component.ts | 2 + .../playground/query-bar-code-snippet.spec.ts | 55 +++++++++++++++++++ .../CodeSnippetTabs/CodeSnippetTabs.tsx | 12 +++- .../PlaygroundView/QueryBar/QueryBar.tsx | 1 + .../SnippetTabs/SnippetTabs.spec.tsx | 8 +++ .../components/SnippetTabs/SnippetTabs.tsx | 32 +++++++++-- .../components/SnippetTabs/StyledWrapper.ts | 27 +++++++++ packages/bruno-api-docs/src/hooks/index.ts | 1 + .../src/hooks/useVariableResolver.tsx | 37 ++++++++++++- 9 files changed, 165 insertions(+), 10 deletions(-) diff --git a/packages/bruno-api-docs/e2e/components/request/code-snippet.component.ts b/packages/bruno-api-docs/e2e/components/request/code-snippet.component.ts index 89134a28..bf6e6288 100644 --- a/packages/bruno-api-docs/e2e/components/request/code-snippet.component.ts +++ b/packages/bruno-api-docs/e2e/components/request/code-snippet.component.ts @@ -8,6 +8,7 @@ export class CodeSnippetComponent extends BaseComponent { readonly iconTrigger: Locator; readonly modal: Locator; readonly modalCode: Locator; + readonly modalInterpolate: Locator; constructor( page: Page, @@ -20,6 +21,7 @@ export class CodeSnippetComponent extends BaseComponent { this.iconTrigger = this.root.getByTestId(`${base}-trigger`); this.modal = page.getByTestId(`${base}-modal`); this.modalCode = this.modal.getByTestId(`${base}-code`); + this.modalInterpolate = this.modal.getByTestId(`${base}-interpolate-input`); } variableToken(name: string): Locator { diff --git a/packages/bruno-api-docs/e2e/tests/playground/query-bar-code-snippet.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/query-bar-code-snippet.spec.ts index dfdb59f7..1a3055d4 100644 --- a/packages/bruno-api-docs/e2e/tests/playground/query-bar-code-snippet.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/playground/query-bar-code-snippet.spec.ts @@ -1,6 +1,9 @@ +import type { Page } from '@playwright/test'; import { test, expect } from '../../playwright'; +import type { PlaygroundComponent } from '../../components/playground.component'; const DESKTOP = { width: 1280, height: 900 }; +const VARS_PLAYGROUND = '/?fixture=vars#/?pg=1&dock=bottom'; test.describe('Playground query bar — code snippet', () => { test.use({ viewport: DESKTOP }); @@ -50,3 +53,55 @@ test.describe('Playground query bar — code snippet', () => { await expect(playground.codeSnippet.modalCode).toContainText('/posts/1/:commentId'); }); }); + +test.describe('Playground code snippet — Interpolate Variables', () => { + test.use({ viewport: DESKTOP }); + + const openVarsPlayground = async (page: Page, playground: PlaygroundComponent) => { + await page.goto(VARS_PLAYGROUND); + await playground.runner.waitFor({ state: 'visible' }); + await playground.openTreeItem(['Customers', 'Variables Demo']); + await playground.envSwitcher.selectEnvironment('Dev'); + }; + + test('the switch resolves variables without closing the playground', async ({ page, playground }) => { + await openVarsPlayground(page, playground); + + const { codeSnippet } = playground; + await codeSnippet.openFromIcon(); + // Starts on, as the app's Generate Code does. + await expect(codeSnippet.modalInterpolate).toBeChecked(); + await expect(codeSnippet.modalCode).toContainText('https://api.dev.example.com/customers/req-42'); + + await codeSnippet.modalInterpolate.setChecked(false); + await expect(codeSnippet.modalCode).toContainText('{{host}}'); + }); + + test('an interpolated playground snippet substitutes secrets, as the app does', async ({ page, playground }) => { + await openVarsPlayground(page, playground); + + const { codeSnippet } = playground; + await codeSnippet.openFromIcon(); + await expect(codeSnippet.modalCode).toContainText('Bearer super-secret-token'); + + await codeSnippet.modalInterpolate.setChecked(false); + await expect(codeSnippet.modalCode).toContainText('Bearer {{bearer_token}}'); + }); + + test('the docs Show vars toggle moves nothing in the playground snippet', async ({ page, playground }) => { + await openVarsPlayground(page, playground); + + const { codeSnippet } = playground; + await codeSnippet.openFromIcon(); + await codeSnippet.modalInterpolate.setChecked(false); + await expect(codeSnippet.modalCode).toContainText('{{host}}'); + + // Flip the app-wide toggle behind the dock: the snippet must not react. + await page.keyboard.press('Escape'); + await playground.envSwitcher.toggle(); + await codeSnippet.openFromIcon(); + await expect(codeSnippet.modalInterpolate).not.toBeChecked(); + await expect(codeSnippet.modalCode).toContainText('{{host}}'); + await expect(codeSnippet.modalCode).not.toContainText('https://api.dev.example.com'); + }); +}); diff --git a/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx b/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx index c4f0704a..db8beea3 100644 --- a/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx +++ b/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx @@ -17,6 +17,7 @@ interface CodeSnippetTabsProps { body?: HttpRequestBody | HttpRequestBodyVariant[]; auth?: Auth; variant?: 'inline' | 'embedded' | 'icon'; + interpolation?: 'showVars' | 'switch'; className?: string; testId?: string; } @@ -34,6 +35,7 @@ export const CodeSnippetTabs: React.FC = ({ body, auth, variant = 'inline', + interpolation, className, testId }) => { @@ -55,7 +57,15 @@ export const CodeSnippetTabs: React.FC = ({ })); }, [method, url, snippetHeaders, body, auth]); - return ; + return ( + + ); }; export default CodeSnippetTabs; diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx index acd53bc5..798e3e41 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx @@ -97,6 +97,7 @@ const QueryBar: React.FC = ({ body={getHttpBody(item)} auth={effectiveAuth} variant="icon" + interpolation="switch" testId="query-bar-code-snippet" /> diff --git a/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.spec.tsx b/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.spec.tsx index 2ffec6b5..9369e6ce 100644 --- a/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.spec.tsx +++ b/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.spec.tsx @@ -85,6 +85,14 @@ describe('SnippetTabs', () => { expect(code.text).toContain('{{host}}'); }); + it('keeps the Interpolate Variables control out of the inline box', () => { + const showVars = useRenderToDom(); + expect(queryByTestId(showVars, 'request-code-snippet-interpolate')).toBeNull(); + + const ownSwitch = useRenderToDom(); + expect(queryByTestId(ownSwitch, 'request-code-snippet-interpolate')).toBeNull(); + }); + it('passes the snippet language through to the highlighter', () => { const root = useRenderToDom( diff --git a/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.tsx b/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.tsx index e95f9e8a..49942cef 100644 --- a/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.tsx +++ b/packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.tsx @@ -3,7 +3,8 @@ import { IconCode } from '@tabler/icons'; import cx from '@/utils/cx'; import { Code } from '../Code/Code'; import { CopyButton } from '@/ui/CopyButton/CopyButton'; -import { useResolvedVariables } from '@/hooks'; +import Checkbox from '@/ui/Checkbox/Checkbox'; +import { ShowVarsOverrideProvider, useResolvedVariables } from '@/hooks'; import { SectionLabel } from '../SectionLabel/SectionLabel'; import { Modal } from '@/ui/Modal/Modal'; import { ExpandIcon } from '@/assets/icons'; @@ -19,6 +20,7 @@ export interface Snippet { interface SnippetTabsProps { snippets: Snippet[]; variant?: 'inline' | 'embedded' | 'icon'; + interpolation?: 'showVars' | 'switch'; className?: string; testId?: string; } @@ -26,6 +28,7 @@ interface SnippetTabsProps { export const SnippetTabs: React.FC = ({ snippets, variant = 'inline', + interpolation = 'showVars', className, testId = 'request-code-snippet' }) => { @@ -33,12 +36,15 @@ export const SnippetTabs: React.FC = ({ const [activeModalId, setActiveModalId] = useState(snippets[0]?.id ?? ''); const [expanded, setExpanded] = useState(false); const triggerRef = useRef(null); - const { showVars, resolve } = useResolvedVariables(); + const { showVars, resolve, interpolate } = useResolvedVariables(); + const ownSwitch = interpolation === 'switch'; + const [shouldInterpolate, setShouldInterpolate] = useState(true); if (snippets.length === 0) return null; const openModal = () => { setActiveModalId(active); + if (!ownSwitch) setShouldInterpolate(showVars); setExpanded(true); }; @@ -50,7 +56,8 @@ export const SnippetTabs: React.FC = ({ const renderSnippetBox = (placement: 'inline' | 'modal', activeId: string, setActiveId: (id: string) => void) => { const activeSnippet = snippets.find((snippet) => snippet.id === activeId) ?? snippets[0]; const code = activeSnippet.code; - const copyText = showVars ? resolve(code) : code; + const isModal = placement === 'modal'; + const copyText = isModal ? (shouldInterpolate ? interpolate(code) : code) : resolve(code); return (
@@ -70,6 +77,17 @@ export const SnippetTabs: React.FC = ({ ))}
+ {isModal && ( + + )} {placement === 'inline' ? (