diff --git a/packages/bruno-api-docs/e2e/components/playground.component.ts b/packages/bruno-api-docs/e2e/components/playground.component.ts index 56847dce..b8e5df11 100644 --- a/packages/bruno-api-docs/e2e/components/playground.component.ts +++ b/packages/bruno-api-docs/e2e/components/playground.component.ts @@ -6,6 +6,7 @@ import { RequestAuthComponent } from './playground/auth.component'; import { MethodSelectorComponent } from './playground/method-selector.component'; import { PlaygroundVariableComponent } from './playground/playground-variable.component'; import { EnvSwitcherComponent } from './layout/env-switcher.component'; +import { CodeSnippetComponent } from './request/code-snippet.component'; import type { DockMode } from '../../src/utils/playgroundDock'; export class PlaygroundComponent extends BaseComponent { @@ -20,7 +21,9 @@ export class PlaygroundComponent extends BaseComponent { readonly testsEditor = new CodeEditorComponent(this.page, 'tests-editor'); readonly variable = new PlaygroundVariableComponent(this.page); readonly envSwitcher = new EnvSwitcherComponent(this.page, 'playground-env-switcher'); + readonly codeSnippet = new CodeSnippetComponent(this.page, 'query-bar-code-snippet'); + readonly urlInput = this.page.getByTestId('query-bar-url'); readonly header = this.page.getByTestId('playground-header'); readonly switcher = this.page.getByTestId('playground-dock-switcher'); readonly content = this.page.getByTestId('playground-content'); 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 0ec3c85d..89134a28 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 @@ -5,6 +5,7 @@ export class CodeSnippetComponent extends BaseComponent { readonly code: Locator; readonly copyButton: Locator; readonly expandButton: Locator; + readonly iconTrigger: Locator; readonly modal: Locator; readonly modalCode: Locator; @@ -16,6 +17,7 @@ export class CodeSnippetComponent extends BaseComponent { this.code = this.root.getByTestId(`${base}-code`); this.copyButton = this.root.getByTestId(`${base}-code-copy`); this.expandButton = this.root.getByTestId(`${base}-expand`); + this.iconTrigger = this.root.getByTestId(`${base}-trigger`); this.modal = page.getByTestId(`${base}-modal`); this.modalCode = this.modal.getByTestId(`${base}-code`); } @@ -46,6 +48,11 @@ export class CodeSnippetComponent extends BaseComponent { await this.modal.waitFor({ state: 'visible' }); } + async openFromIcon(): Promise { + await this.iconTrigger.click(); + await this.modal.waitFor({ state: 'visible' }); + } + async selectModalLanguage(language: string): Promise { await this.modalLanguageTab(language).click(); } 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 new file mode 100644 index 00000000..dfdb59f7 --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/playground/query-bar-code-snippet.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '../../playwright'; + +const DESKTOP = { width: 1280, height: 900 }; + +test.describe('Playground query bar — code snippet', () => { + test.use({ viewport: DESKTOP }); + + test.beforeEach(async ({ playground }) => { + await playground.open('bottom'); + }); + + test('the query bar offers an icon-only snippet control that opens the snippet modal', async ({ playground }) => { + await playground.openRequest('get users'); + + const { codeSnippet } = playground; + await expect(codeSnippet.iconTrigger).toBeVisible(); + await expect(codeSnippet.iconTrigger).toHaveAttribute('aria-label', 'Generate Code'); + // Icon only — the code box lives in the modal. + await expect(codeSnippet.code).toHaveCount(0); + + await codeSnippet.openFromIcon(); + await expect(codeSnippet.modalCode).toContainText('curl'); + }); + + test('switches languages inside the modal', async ({ playground }) => { + await playground.openRequest('get users'); + await playground.codeSnippet.openFromIcon(); + + await playground.codeSnippet.selectModalLanguage('python'); + await expect(playground.codeSnippet.modalLanguageTab('python')).toHaveAttribute('aria-selected', 'true'); + await expect(playground.codeSnippet.modalCode).toContainText('requests'); + }); + + test('the snippet url substitutes filled path params and keeps unfilled placeholders', async ({ + page, + playground + }) => { + await playground.openRequest('Jokes'); + await playground.codeSnippet.openFromIcon(); + await expect(playground.codeSnippet.modalCode).toContainText('/posts/1'); + await page.keyboard.press('Escape'); + + // A fresh `:commentId` segment is a path param with no value yet. + await playground.urlInput.click(); + await page.keyboard.press('End'); + await page.keyboard.type('/:commentId'); + + await playground.codeSnippet.openFromIcon(); + // Empty path params keep their placeholder instead of collapsing. + await expect(playground.codeSnippet.modalCode).toContainText('/posts/1/:commentId'); + }); +}); diff --git a/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx b/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx index 0c6c908d..c4f0704a 100644 --- a/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx +++ b/packages/bruno-api-docs/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx @@ -16,7 +16,7 @@ interface CodeSnippetTabsProps { headers?: HttpRequestHeader[]; body?: HttpRequestBody | HttpRequestBodyVariant[]; auth?: Auth; - variant?: 'inline' | 'embedded'; + variant?: 'inline' | 'embedded' | 'icon'; className?: string; testId?: string; } diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx index 86ffe1f8..fbd7d036 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import type { HttpRequest } from '@opencollection/types/requests/http'; +import type { HttpRequest, HttpRequestHeader } from '@opencollection/types/requests/http'; import type { OpenCollection as OpenCollectionCollection } from '@opencollection/types'; import type { Item } from '@opencollection/types/collection/item'; +import type { Auth } from '@opencollection/types/common/auth'; import { requestRunner } from '@/runner'; import { getAncestorsByUuid } from '@/utils/fileUtils'; import { ItemVariableResolverProvider } from '@/hooks'; @@ -10,9 +11,14 @@ import QueryBar from './QueryBar/QueryBar'; import RequestPane from './RequestPane/RequestPane'; import ResponsePane from './ResponsePane/ResponsePane'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; -import { updatePlaygroundItem, setPlaygroundResponse, selectPlaygroundResponse, applyScriptVariableChanges } from '@/store/slices/playground'; -import { getItemName, isPlaygroundUnsupported } from '@/utils/schemaHelpers'; -import { getInheritedAuthSummary } from '@/utils/request'; +import { + updatePlaygroundItem, + setPlaygroundResponse, + selectPlaygroundResponse, + applyScriptVariableChanges +} from '@/store/slices/playground'; +import { getItemName, isPlaygroundUnsupported, getRequestAuth, getRequestHeaders } from '@/utils/schemaHelpers'; +import { getInheritedAuthSummary, resolveInheritedAuth, getInheritedHeaders } from '@/utils/request'; import UnsupportedRequest from '@/components/UnsupportedRequest/UnsupportedRequest'; import { FileNotFoundIcon } from '@/assets/icons'; import { useSplitPane } from '@/hooks/useSplitPane'; @@ -44,6 +50,27 @@ const HttpRequestPlaygroundView: React.FC = ({ item, collec () => getInheritedAuthSummary(collection, ancestry, editableItem), [collection, ancestry, editableItem] ); + // Resolve the auth so that the runner and the code snippet show the same effective auth. + const effectiveAuth = useMemo(() => { + const ownAuth = getRequestAuth(editableItem) as Auth | undefined; + return ownAuth === 'inherit' ? resolveInheritedAuth(collection, ancestry, editableItem).auth : ownAuth; + }, [collection, ancestry, editableItem]); + + // Applies same rules as runner so that the code snippet shows the same effective headers as the runner. + const effectiveHeaders = useMemo(() => { + const auth = effectiveAuth && effectiveAuth !== 'inherit' ? effectiveAuth : undefined; + const authWritesAuthorization = Boolean( + (auth?.type === 'bearer' && auth.token) || (auth?.type === 'basic' && auth.username && auth.password) + ); + const keep = (header: { name?: string }) => + !authWritesAuthorization || (header.name || '').toLowerCase() !== 'authorization'; + const ownRows = getRequestHeaders(editableItem).filter(keep); + const inheritedRows = getInheritedHeaders(collection, ancestry, editableItem) + .filter(keep) + .map((header) => ({ name: header.name, value: header.value ?? '', disabled: header.disabled })); + return [...ownRows, ...inheritedRows]; + }, [collection, ancestry, editableItem, effectiveAuth]); + const saveTimeoutRef = useRef(null); const pendingSaveRef = useRef<{ uuid: string; item: HttpRequest } | null>(null); @@ -129,6 +156,8 @@ const HttpRequestPlaygroundView: React.FC = ({ item, collec onSendRequest={handleSendRequest} isLoading={isLoading} onItemChange={handleItemChange} + effectiveAuth={effectiveAuth} + effectiveHeaders={effectiveHeaders} />
{}} isLoading={false} onItemChange={() => {}} />; + +describe('Playground QueryBar — code snippet', () => { + it('offers the code-snippet control alongside the copy-url action', () => { + const root = useRenderToDom(queryBar); + + expect(queryByTestId(root, 'query-bar-code-snippet-trigger')).not.toBeNull(); + expect(queryByTestId(root, 'query-bar-copy-url')).not.toBeNull(); + }); +}); 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 26defe8c..acd53bc5 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 @@ -1,22 +1,33 @@ import React, { useState, useEffect } from 'react'; -import type { HttpRequest } from '@opencollection/types/requests/http'; +import type { HttpRequest, HttpRequestParam, HttpRequestHeader } from '@opencollection/types/requests/http'; +import type { Auth } from '@opencollection/types/common/auth'; import { StyledWrapper } from './StyledWrapper'; import HighlightedInput from '@/components/HighlightedInput/HighlightedInput'; import { useResolvedVariables } from '@/hooks/useVariableResolver'; -import { getHttpMethod, getRequestUrl, getHttpParams } from '@/utils/schemaHelpers'; -import { syncPathParams, syncQueryParams } from '@/utils/pathParams'; +import { getHttpMethod, getRequestUrl, getHttpParams, getRequestHeaders, getHttpBody } from '@/utils/schemaHelpers'; +import { buildRequestUrl, syncPathParams, syncQueryParams } from '@/utils/pathParams'; import { HttpMethodSelector } from '@/components/HttpMethodSelector/HttpMethodSelector'; import { CopyButton } from '@/ui/CopyButton/CopyButton'; import { SendIcon } from '@/assets/icons'; +import { CodeSnippetTabs } from '@/components/CodeSnippetTabs/CodeSnippetTabs'; interface QueryBarProps { item: HttpRequest; onSendRequest: () => void; isLoading: boolean; onItemChange: (item: HttpRequest) => void; + effectiveAuth?: Auth; + effectiveHeaders?: HttpRequestHeader[]; } -const QueryBar: React.FC = ({ item, onSendRequest, isLoading, onItemChange }) => { +const QueryBar: React.FC = ({ + item, + onSendRequest, + isLoading, + onItemChange, + effectiveAuth, + effectiveHeaders +}) => { const { isFound, names } = useResolvedVariables(); const [url, setUrl] = useState(getRequestUrl(item)); const [method, setMethod] = useState(getHttpMethod(item)); @@ -55,6 +66,11 @@ const QueryBar: React.FC = ({ item, onSendRequest, isLoading, onI onItemChange(updatedItem); }; + const snippetUrl = buildRequestUrl( + url, + getHttpParams(item).filter((param: HttpRequestParam) => param.type !== 'path' || (param.value ?? '').trim() !== '') + ); + return ( @@ -74,6 +90,15 @@ const QueryBar: React.FC = ({ item, onSendRequest, isLoading, onI />
+ )}