From a1cc10ac0d9aa42ab3b46fdbc1f645ebca2d35bd Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Wed, 23 Sep 2026 17:22:38 +0530 Subject: [PATCH 1/5] fix(docs): parity UI bugs 2, group 3 - gRPC request page keeps the URL bar pinned while scrolling - Show vars switch and theme toggle get themed tooltips - script editor search box tooltips render outside the editor - example card scrollbars show only on hover - truncated text no longer cuts off letters like g, y, p - search method badges show 4-5 letter methods in full - tooltips follow the light and dark theme - description tooltips keep their line breaks - remove collection-level tags from the overview - sample collection gains an OPTIONS request and long environment names --- .../e2e/components/base.component.ts | 12 +++++++ .../code-editor/code-editor.component.ts | 14 ++++++++ .../layout/env-switcher.component.ts | 1 + .../layout/theme-toggle.component.ts | 1 + .../e2e/components/search/search.component.ts | 4 +++ .../e2e/components/tooltip.component.ts | 17 ++++++++++ .../e2e/pages/grpc-request.page.ts | 1 + .../tests/environments/environments.spec.ts | 2 +- .../e2e/tests/layout/page-header.spec.ts | 16 +++++++++ .../e2e/tests/overview/overview.spec.ts | 6 ++-- .../code-editor-find-widget.spec.ts | 25 ++++++++++++++ .../e2e/tests/request/grpc-request.spec.ts | 14 ++++++++ .../tests/request/request-examples.spec.ts | 11 ++++++ .../e2e/tests/search/search.spec.ts | 16 +++++++++ .../e2e/tests/theming/theme-toggle.spec.ts | 6 ++++ .../tests/tooltip/truncation-tooltip.spec.ts | 34 +++++++++++++++++++ .../variableInfoPopup.spec.ts | 8 +++++ .../components/Description/Description.tsx | 2 +- .../Examples/ExampleCard/StyledWrapper.ts | 16 +++++++-- .../components/PropertyTable/StyledWrapper.ts | 7 +++- .../Search/SearchResultItem/StyledWrapper.ts | 7 ++-- .../ShowVarsToggle/ShowVarsToggle.tsx | 29 +++++++++------- .../components/ThemeToggle/ThemeToggle.tsx | 16 ++++----- .../components/Topbar/Brand/StyledWrapper.ts | 3 +- .../TruncatedText/TruncatedText.tsx | 4 ++- .../VariableInfoCard/StyledWrapper.ts | 3 +- .../src/e2eFixtures/descriptionsCollection.ts | 8 ++++- .../src/e2eFixtures/qaCollection.ts | 31 +++++++++++++---- .../src/pages/Environments/StyledWrapper.ts | 6 ++-- .../src/pages/GrpcRequest/GrpcRequest.tsx | 5 ++- .../src/pages/GrpcRequest/StyledWrapper.ts | 10 ++++-- .../src/pages/Overview/Overview.spec.tsx | 11 ++++++ .../src/pages/Overview/Overview.tsx | 5 +-- .../src/pages/Overview/StyledWrapper.ts | 3 -- .../bruno-api-docs/src/sampleCollection.ts | 34 ++++++++++++++++++- .../src/ui/CodeEditor/CodeEditor.tsx | 14 +++++++- .../src/ui/CodeEditor/StyledWrapper.ts | 12 +++++++ .../src/ui/CodeEditor/contextViewPortal.ts | 31 +++++++++++++++++ .../src/ui/Tooltip/StyledWrapper.ts | 13 ++++--- .../bruno-api-docs/src/ui/Tooltip/Tooltip.tsx | 4 ++- .../src/utils/schemaHelpers.spec.ts | 14 -------- .../bruno-api-docs/src/utils/schemaHelpers.ts | 4 --- 42 files changed, 399 insertions(+), 81 deletions(-) create mode 100644 packages/bruno-api-docs/e2e/tests/playground/code-editor-find-widget.spec.ts create mode 100644 packages/bruno-api-docs/src/ui/CodeEditor/contextViewPortal.ts diff --git a/packages/bruno-api-docs/e2e/components/base.component.ts b/packages/bruno-api-docs/e2e/components/base.component.ts index 3c7f6ed9..2b3d46d0 100644 --- a/packages/bruno-api-docs/e2e/components/base.component.ts +++ b/packages/bruno-api-docs/e2e/components/base.component.ts @@ -9,6 +9,18 @@ export abstract class BaseComponent { this.root = root ?? page.locator(':root'); } + async hidesVerticalOverflow(target: Locator): Promise { + return target.evaluate((el) => getComputedStyle(el).overflowY !== 'visible'); + } + + async isContentWiderThanBox(target: Locator): Promise { + return target.evaluate((el) => el.scrollWidth > el.clientWidth + 1); + } + + async getScrollbarColor(target: Locator): Promise { + return target.evaluate((el) => getComputedStyle(el).scrollbarColor); + } + /** Press the pointer on a resize handle; the grab point is kept for later moves. */ protected async grabHandle(handle: Locator): Promise { const box = await handle.boundingBox(); diff --git a/packages/bruno-api-docs/e2e/components/code-editor/code-editor.component.ts b/packages/bruno-api-docs/e2e/components/code-editor/code-editor.component.ts index c0082081..5b6cdd82 100644 --- a/packages/bruno-api-docs/e2e/components/code-editor/code-editor.component.ts +++ b/packages/bruno-api-docs/e2e/components/code-editor/code-editor.component.ts @@ -9,6 +9,8 @@ import { BaseComponent } from '../base.component'; export class CodeEditorComponent extends BaseComponent { readonly copyButton: Locator; readonly suggestions: Locator; + readonly searchBox: Locator; + readonly contextViewHover: Locator; private readonly surface: Locator; private readonly focused: Locator; private readonly lines: Locator; @@ -21,6 +23,8 @@ export class CodeEditorComponent extends BaseComponent { this.focused = this.root.locator('.monaco-editor.focused'); this.lines = this.root.locator('.view-lines'); this.suggestions = page.locator('.suggest-widget.visible'); + this.searchBox = this.root.locator('.find-widget.visible'); + this.contextViewHover = page.getByTestId(`${testId}-context-view-host`).locator('.monaco-hover'); this.ready = page.locator(`[data-testid="${testId}"][data-editor-ready="true"]`); } @@ -38,4 +42,14 @@ export class CodeEditorComponent extends BaseComponent { await this.page.keyboard.type(text, { delay: 30 }); await this.page.keyboard.press('Control+Space'); } + + async openSearchBox(): Promise { + await this.focus(); + await this.page.keyboard.press('Control+f'); + await this.searchBox.waitFor({ state: 'visible' }); + } + + searchBoxButton(label: RegExp): Locator { + return this.searchBox.getByLabel(label); + } } diff --git a/packages/bruno-api-docs/e2e/components/layout/env-switcher.component.ts b/packages/bruno-api-docs/e2e/components/layout/env-switcher.component.ts index 70cfe96e..eb9c23bb 100644 --- a/packages/bruno-api-docs/e2e/components/layout/env-switcher.component.ts +++ b/packages/bruno-api-docs/e2e/components/layout/env-switcher.component.ts @@ -12,6 +12,7 @@ import { BaseComponent } from '../base.component'; */ export class EnvSwitcherComponent extends BaseComponent { readonly showVarsToggle = this.page.getByTestId('show-vars-toggle'); + readonly showVarsTooltip = this.page.getByTestId('show-vars-toggle-tooltip'); readonly trigger: Locator; readonly menu: Locator; readonly surface: Locator; diff --git a/packages/bruno-api-docs/e2e/components/layout/theme-toggle.component.ts b/packages/bruno-api-docs/e2e/components/layout/theme-toggle.component.ts index dd9d1922..39dd6f7f 100644 --- a/packages/bruno-api-docs/e2e/components/layout/theme-toggle.component.ts +++ b/packages/bruno-api-docs/e2e/components/layout/theme-toggle.component.ts @@ -2,6 +2,7 @@ import { BaseComponent } from '../base.component'; export class ThemeToggleComponent extends BaseComponent { readonly button = this.page.getByTestId('theme-toggle'); + readonly tooltip = this.page.getByTestId('theme-toggle-tooltip'); async toggle(): Promise { await this.button.click(); diff --git a/packages/bruno-api-docs/e2e/components/search/search.component.ts b/packages/bruno-api-docs/e2e/components/search/search.component.ts index baa2a1e9..635ffce8 100644 --- a/packages/bruno-api-docs/e2e/components/search/search.component.ts +++ b/packages/bruno-api-docs/e2e/components/search/search.component.ts @@ -48,4 +48,8 @@ export class SearchComponent extends BaseComponent { result(text: string): Locator { return this.results.filter({ hasText: text }); } + + resultMethod(text: string): Locator { + return this.result(text).getByTestId('search-result-method'); + } } diff --git a/packages/bruno-api-docs/e2e/components/tooltip.component.ts b/packages/bruno-api-docs/e2e/components/tooltip.component.ts index cee2d6e0..bbdb9fed 100644 --- a/packages/bruno-api-docs/e2e/components/tooltip.component.ts +++ b/packages/bruno-api-docs/e2e/components/tooltip.component.ts @@ -32,4 +32,21 @@ export class TooltipComponent extends BaseComponent { async hoverCell(index: number): Promise { await this.truncatableCells.nth(index).hover(); } + + cellWithText(text: string): Locator { + return this.truncatableCells.filter({ hasText: text }); + } + + async popupColorsAgainstTheme(): Promise<{ popup: string[]; theme: string[] }> { + return this.popup.evaluate((el) => { + const probe = document.createElement('div'); + probe.style.background = 'var(--oc-background-surface0)'; + probe.style.color = 'var(--oc-text)'; + document.body.appendChild(probe); + const theme = [getComputedStyle(probe).backgroundColor, getComputedStyle(probe).color]; + probe.remove(); + const style = getComputedStyle(el); + return { popup: [style.backgroundColor, style.color], theme }; + }); + } } diff --git a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts index ea94cd63..1a1bc930 100644 --- a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts +++ b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts @@ -18,6 +18,7 @@ export class GrpcRequestPage extends BasePage { readonly root: Locator = this.page.getByTestId('grpc-request-page'); readonly title: Locator = this.page.getByTestId('grpc-request-title'); readonly description: Locator = this.page.getByTestId('grpc-request-description'); + readonly urlSticky: Locator = this.page.getByTestId('grpc-request-url-sticky'); readonly protoFileSection: Locator = this.page.getByTestId('grpc-request-section-proto-file'); readonly protoFile: Locator = this.page.getByTestId('grpc-request-proto-file'); diff --git a/packages/bruno-api-docs/e2e/tests/environments/environments.spec.ts b/packages/bruno-api-docs/e2e/tests/environments/environments.spec.ts index 96b37866..f5a1afa6 100644 --- a/packages/bruno-api-docs/e2e/tests/environments/environments.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/environments/environments.spec.ts @@ -7,7 +7,7 @@ test.describe('Environments page', () => { test('opens from the sidebar and shows a tab per environment', async ({ environmentsPage }) => { await expect(environmentsPage.title).toHaveText('Environments'); - await expect(environmentsPage.tabs).toHaveCount(2); + await expect(environmentsPage.tabs).toHaveCount(4); await expect(environmentsPage.tab('Local')).toBeVisible(); await expect(environmentsPage.tab('Prod')).toBeVisible(); }); diff --git a/packages/bruno-api-docs/e2e/tests/layout/page-header.spec.ts b/packages/bruno-api-docs/e2e/tests/layout/page-header.spec.ts index e5a74763..721497f2 100644 --- a/packages/bruno-api-docs/e2e/tests/layout/page-header.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/layout/page-header.spec.ts @@ -27,6 +27,22 @@ test.describe('Page header', () => { expect(box.y).toBeLessThanOrEqual(1); }); + test('shows the full height of the brand name, descenders included', async ({ page, pageHeader }) => { + await page.setViewportSize(DESKTOP); + await page.goto('/'); + + expect(await pageHeader.hidesVerticalOverflow(pageHeader.brandName)).toBe(false); + }); + + test('explains the Show vars switch in a tooltip on hover', async ({ page, envSwitcher }) => { + await page.setViewportSize(DESKTOP); + await page.goto('/'); + + await envSwitcher.showVarsToggle.hover(); + + await expect(envSwitcher.showVarsTooltip).toHaveText('Show resolved variable values'); + }); + test('shows the initials avatar derived from the collection name', async ({ page, pageHeader }) => { await page.setViewportSize(DESKTOP); await page.goto('/'); diff --git a/packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts b/packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts index aab31cee..ac91cda5 100644 --- a/packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts @@ -22,11 +22,11 @@ test.describe('Collection Overview', () => { }); }); - test('shows three stat cards with the request (61), folder (14) and environment (2) counts', async ({ overviewPage }) => { + test('shows three stat cards with the request (62), folder (14) and environment (4) counts', async ({ overviewPage }) => { await expect(overviewPage.stats.cards).toHaveCount(3); - await expect(overviewPage.stats.valueFor('Requests')).toHaveText('61'); + await expect(overviewPage.stats.valueFor('Requests')).toHaveText('62'); await expect(overviewPage.stats.valueFor('Folders')).toHaveText('14'); - await expect(overviewPage.stats.valueFor('Environments')).toHaveText('2'); + await expect(overviewPage.stats.valueFor('Environments')).toHaveText('4'); }); test('renders the collection documentation under the "Overview" section', async ({ overviewPage }) => { diff --git a/packages/bruno-api-docs/e2e/tests/playground/code-editor-find-widget.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/code-editor-find-widget.spec.ts new file mode 100644 index 00000000..17bb5fa2 --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/playground/code-editor-find-widget.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '../../playwright'; + +test.describe('Playground code editor: find widget tooltips', () => { + test.beforeEach(async ({ playground }) => { + await playground.open('bottom'); + await playground.openRequest('get users'); + await playground.selectTab('scripts'); + await playground.preRequestScriptEditor.openSearchBox(); + }); + + for (const label of [/^Find in Selection/, /^Close/, /^Previous Match/]) { + test(`the "${label.source.slice(1)}" tooltip renders outside the editor, on one line, above its button`, async ({ playground }) => { + const editor = playground.preRequestScriptEditor; + const button = editor.searchBoxButton(label); + + await button.hover(); + await expect(editor.contextViewHover).toBeVisible(); + + const buttonBox = await button.boundingBox(); + const hoverBox = await editor.contextViewHover.boundingBox(); + expect(hoverBox!.y + hoverBox!.height).toBeLessThanOrEqual(buttonBox!.y); + expect(hoverBox!.height).toBeLessThan(buttonBox!.height * 2); + }); + } +}); diff --git a/packages/bruno-api-docs/e2e/tests/request/grpc-request.spec.ts b/packages/bruno-api-docs/e2e/tests/request/grpc-request.spec.ts index 4c1bcb8b..4e3eb4d4 100644 --- a/packages/bruno-api-docs/e2e/tests/request/grpc-request.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/request/grpc-request.spec.ts @@ -13,6 +13,20 @@ test.describe('Request page — gRPC requests', () => { await expect(grpcRequestPage.urlBar.tryButton).toHaveCount(0); }); + test('keeps the URL bar pinned below the header while the page scrolls', async ({ page, grpcRequestPage }) => { + await page.setViewportSize({ width: 1280, height: 600 }); + await grpcRequestPage.open([REALTIME, 'Order Service']); + const before = await grpcRequestPage.urlSticky.boundingBox(); + + await grpcRequestPage.urlSticky.hover(); + await page.mouse.wheel(0, 900); + + await expect(grpcRequestPage.title).not.toBeInViewport(); + await expect(grpcRequestPage.urlSticky).toBeInViewport(); + const after = await grpcRequestPage.urlSticky.boundingBox(); + expect(after!.y).toBeLessThan(before!.y); + }); + test('marks the grpcurl command plaintext for an unencrypted environment', async ({ grpcRequestPage }) => { await grpcRequestPage.open([REALTIME, 'Order Service']); diff --git a/packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts b/packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts index 3c15cbf1..110ee251 100644 --- a/packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts @@ -77,6 +77,17 @@ test.describe('Request page — Examples', () => { await expect(examples.responseBody(OK_EXAMPLE)).toContainText('john.smith@example.com'); }); + test('keeps the response scrollbar hidden until the pane is hovered', async ({ page, requestPage }) => { + const { examples } = requestPage; + const body = examples.responseBody(OK_EXAMPLE); + await page.mouse.move(0, 0); + + expect(await examples.getScrollbarColor(body)).toBe('rgba(0, 0, 0, 0) rgba(0, 0, 0, 0)'); + + await body.hover(); + expect(await examples.getScrollbarColor(body)).not.toBe('rgba(0, 0, 0, 0) rgba(0, 0, 0, 0)'); + }); + test('switches to the Headers tab to reveal the response headers', async ({ requestPage }) => { const { examples } = requestPage; await examples.selectResponseTab(OK_EXAMPLE, 'headers'); diff --git a/packages/bruno-api-docs/e2e/tests/search/search.spec.ts b/packages/bruno-api-docs/e2e/tests/search/search.spec.ts index b7e6b43f..0db84be9 100644 --- a/packages/bruno-api-docs/e2e/tests/search/search.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/search/search.spec.ts @@ -76,6 +76,22 @@ test.describe('Search palette', () => { await expect(search.panel).toContainText('Login'); }); + test('shows a five-letter method in full and shortens a longer one to three letters, never with an ellipsis', async ({ page, search }) => { + await page.setViewportSize(DESKTOP); + await page.goto(FIXTURE); + await search.field.click(); + + await search.field.fill('confirm booking'); + const patch = search.resultMethod('Confirm Booking'); + await expect(patch).toHaveText('PATCH'); + expect(await search.isContentWiderThanBox(patch)).toBe(false); + + await search.field.fill('cancel booking'); + const del = search.resultMethod('Cancel Booking'); + await expect(del).toHaveText('DEL'); + expect(await search.isContentWiderThanBox(del)).toBe(false); + }); + test('corrects a typo (adjacent letter swap) to the intended request', async ({ page, search }) => { await page.setViewportSize(DESKTOP); await page.goto(FIXTURE); diff --git a/packages/bruno-api-docs/e2e/tests/theming/theme-toggle.spec.ts b/packages/bruno-api-docs/e2e/tests/theming/theme-toggle.spec.ts index 0559d4c3..0e91881a 100644 --- a/packages/bruno-api-docs/e2e/tests/theming/theme-toggle.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/theming/theme-toggle.spec.ts @@ -15,6 +15,12 @@ test.describe('Theme switcher', () => { await expect(themeToggle.button).toHaveAccessibleName('Switch to dark theme'); }); + test('names the action in a tooltip on hover', async ({ themeToggle }) => { + await themeToggle.button.hover(); + + await expect(themeToggle.tooltip).toHaveText('Switch to dark theme'); + }); + test('switches the whole app to dark mode when the toggle is clicked', async ({ page, themeToggle }) => { await themeToggle.toggle(); diff --git a/packages/bruno-api-docs/e2e/tests/tooltip/truncation-tooltip.spec.ts b/packages/bruno-api-docs/e2e/tests/tooltip/truncation-tooltip.spec.ts index a6ae99b7..02ee51cf 100644 --- a/packages/bruno-api-docs/e2e/tests/tooltip/truncation-tooltip.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/tooltip/truncation-tooltip.spec.ts @@ -30,3 +30,37 @@ test.describe('Truncation tooltips', () => { await expect(tooltip.popup).toHaveCount(0); }); }); + +test.describe('Description tooltips', () => { + const DESCRIBED_REQUEST = '/?fixture=descriptions#/described-request'; + const MULTI_PARAGRAPH = 'Identifies this request across every service that handles it, from the gateway to the database.\n\nSend a fresh value per call so retries can be told apart in the logs.'; + + test('keeps the line breaks of a multi-paragraph description', async ({ page, tooltip }) => { + await page.goto(DESCRIBED_REQUEST); + + await tooltip.cellWithText('Identifies this request').hover(); + + await expect(tooltip.popup).toBeVisible(); + expect(await tooltip.popup.innerText()).toBe(MULTI_PARAGRAPH); + }); +}); + +test.describe('Tooltip theme', () => { + test.use({ colorScheme: 'light' }); + + test('uses the theme surface and text colors in light and dark mode', async ({ page, tooltip, themeToggle }) => { + await page.goto('/?fixture=descriptions#/described-request'); + + await tooltip.cellWithText('Identifies this request').hover(); + await expect(tooltip.popup).toBeVisible(); + const light = await tooltip.popupColorsAgainstTheme(); + expect(light.popup).toEqual(light.theme); + + await themeToggle.toggle(); + await tooltip.cellWithText('Identifies this request').hover(); + await expect(tooltip.popup).toBeVisible(); + const dark = await tooltip.popupColorsAgainstTheme(); + expect(dark.popup).toEqual(dark.theme); + expect(dark.popup[0]).not.toBe(light.popup[0]); + }); +}); diff --git a/packages/bruno-api-docs/e2e/tests/variableInfoPopup/variableInfoPopup.spec.ts b/packages/bruno-api-docs/e2e/tests/variableInfoPopup/variableInfoPopup.spec.ts index 3c06e409..112fd41c 100644 --- a/packages/bruno-api-docs/e2e/tests/variableInfoPopup/variableInfoPopup.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/variableInfoPopup/variableInfoPopup.spec.ts @@ -18,6 +18,14 @@ test.describe('Variable hover card', () => { await expect(variableCard.value).toHaveText('https://api.dev.example.com'); }); + test('shows the full height of the variable name, descenders included', async ({ requestPage }) => { + const { variableCard } = requestPage; + await variableCard.hoverToken('apiVersion'); + + await expect(variableCard.name).toBeVisible(); + expect(await variableCard.hidesVerticalOverflow(variableCard.name)).toBe(false); + }); + test('badges collection, folder and request scopes correctly', async ({ page, requestPage }) => { const { variableCard } = requestPage; await variableCard.hoverToken('apiVersion'); diff --git a/packages/bruno-api-docs/src/components/Description/Description.tsx b/packages/bruno-api-docs/src/components/Description/Description.tsx index 7e2cbf0d..e1e8144c 100644 --- a/packages/bruno-api-docs/src/components/Description/Description.tsx +++ b/packages/bruno-api-docs/src/components/Description/Description.tsx @@ -9,7 +9,7 @@ interface DescriptionProps { export const Description: React.FC = ({ text, className }) => { const trimmed = text?.trim(); return trimmed ? ( - + ) : null; }; diff --git a/packages/bruno-api-docs/src/components/Examples/ExampleCard/StyledWrapper.ts b/packages/bruno-api-docs/src/components/Examples/ExampleCard/StyledWrapper.ts index 293e67d9..7a5e1aaa 100644 --- a/packages/bruno-api-docs/src/components/Examples/ExampleCard/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/Examples/ExampleCard/StyledWrapper.ts @@ -233,16 +233,28 @@ export const StyledWrapper = styled.div` overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; + } + .pane-body, + .pane-body .code-content { + scrollbar-color: transparent transparent; + } + .pane-body:hover, + .pane-body .code-content:hover { scrollbar-color: var(--oc-scrollbar-color) transparent; } .pane-body::-webkit-scrollbar { width: 6px; height: 6px; } - .pane-body::-webkit-scrollbar-thumb { - background-color: var(--oc-scrollbar-color); + .pane-body::-webkit-scrollbar-thumb, + .pane-body .code-content::-webkit-scrollbar-thumb { + background-color: transparent; border-radius: 3px; } + .pane-body:hover::-webkit-scrollbar-thumb, + .pane-body .code-content:hover::-webkit-scrollbar-thumb { + background-color: var(--oc-scrollbar-color); + } .pane-body::-webkit-scrollbar-track { background: transparent; } diff --git a/packages/bruno-api-docs/src/components/PropertyTable/StyledWrapper.ts b/packages/bruno-api-docs/src/components/PropertyTable/StyledWrapper.ts index 2004874f..750706cd 100644 --- a/packages/bruno-api-docs/src/components/PropertyTable/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/PropertyTable/StyledWrapper.ts @@ -79,9 +79,14 @@ export const StyledWrapper = styled.div` letter-spacing: normal; color: var(--text-primary); white-space: nowrap; - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; } + .property-key .oc-truncate { + overflow-x: clip; + overflow-y: visible; + } .property-value-cell { margin: 0; min-width: 0; diff --git a/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts b/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts index b28ddd38..68dc61b2 100644 --- a/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts @@ -25,7 +25,7 @@ export const StyledWrapper = styled.button` .search-result-method { flex-shrink: 0; display: inline-block; - width: 26px; + min-width: 26px; margin-top: 1px; text-align: left; font-family: var(--font-mono); @@ -33,8 +33,6 @@ export const StyledWrapper = styled.button` font-size: 10.5px; letter-spacing: 0.42px; color: var(--method-color); - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; } @@ -117,7 +115,8 @@ export const StyledWrapper = styled.button` font-size: 11px; line-height: 1.2; color: var(--oc-colors-text-subtext1); - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx b/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx index 5d5d6f9a..6b90f180 100644 --- a/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx +++ b/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { selectShowVars, toggleShowVars } from '@/store/slices/env'; +import { Tooltip } from '@/ui/Tooltip/Tooltip'; import { StyledWrapper } from './StyledWrapper'; export interface ShowVarsToggleProps { @@ -17,19 +18,21 @@ const ShowVarsToggle: React.FC = ({ testId = 'show-vars-tog const showVars = useAppSelector(selectShowVars); return ( - dispatch(toggleShowVars())} - > - + + dispatch(toggleShowVars())} + > + + ); }; diff --git a/packages/bruno-api-docs/src/components/ThemeToggle/ThemeToggle.tsx b/packages/bruno-api-docs/src/components/ThemeToggle/ThemeToggle.tsx index ffb2d780..8bf83b9f 100644 --- a/packages/bruno-api-docs/src/components/ThemeToggle/ThemeToggle.tsx +++ b/packages/bruno-api-docs/src/components/ThemeToggle/ThemeToggle.tsx @@ -2,6 +2,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { toggleTheme } from '@/store/slices/theme'; +import { Tooltip } from '@/ui/Tooltip/Tooltip'; const Button = styled.button` display: inline-flex; @@ -45,17 +46,14 @@ const ThemeToggle: React.FC<{ testId?: string }> = ({ testId = 'theme-toggle' }) const dispatch = useAppDispatch(); const mode = useAppSelector((s) => s.theme.mode); const isDark = mode === 'dark'; + const label = isDark ? 'Switch to light theme' : 'Switch to dark theme'; return ( - + + + ); }; diff --git a/packages/bruno-api-docs/src/components/Topbar/Brand/StyledWrapper.ts b/packages/bruno-api-docs/src/components/Topbar/Brand/StyledWrapper.ts index fbd46ef8..65e67cf4 100644 --- a/packages/bruno-api-docs/src/components/Topbar/Brand/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/Topbar/Brand/StyledWrapper.ts @@ -38,7 +38,8 @@ export const StyledWrapper = styled.div` font-weight: 600; color: var(--oc-text); white-space: nowrap; - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; } diff --git a/packages/bruno-api-docs/src/components/TruncatedText/TruncatedText.tsx b/packages/bruno-api-docs/src/components/TruncatedText/TruncatedText.tsx index feeee030..fbdd52b8 100644 --- a/packages/bruno-api-docs/src/components/TruncatedText/TruncatedText.tsx +++ b/packages/bruno-api-docs/src/components/TruncatedText/TruncatedText.tsx @@ -6,6 +6,7 @@ export interface TruncatedTextProps { text: string; children?: ReactNode; touch?: boolean; + multilineTooltip?: boolean; className?: string; testId?: string; } @@ -17,10 +18,11 @@ export const TruncatedText: React.FC = ({ text, children, touch, + multilineTooltip, className, testId = 'truncated-text' }) => ( - + {children ?? text} diff --git a/packages/bruno-api-docs/src/components/VariableInfoCard/StyledWrapper.ts b/packages/bruno-api-docs/src/components/VariableInfoCard/StyledWrapper.ts index 2d97f4df..3afd7631 100644 --- a/packages/bruno-api-docs/src/components/VariableInfoCard/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/VariableInfoCard/StyledWrapper.ts @@ -28,7 +28,8 @@ export const StyledWrapper = styled.div` .var-name { flex: 1; min-width: 0; - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; diff --git a/packages/bruno-api-docs/src/e2eFixtures/descriptionsCollection.ts b/packages/bruno-api-docs/src/e2eFixtures/descriptionsCollection.ts index 56279e4e..1068424d 100644 --- a/packages/bruno-api-docs/src/e2eFixtures/descriptionsCollection.ts +++ b/packages/bruno-api-docs/src/e2eFixtures/descriptionsCollection.ts @@ -2,7 +2,8 @@ import type { OpenCollection } from '@opencollection/types'; // A single request carrying authored descriptions on every editable surface (query params, headers, // pre-request variables, form-urlencoded body fields), used to prove the playground shows and edits -// them. One header uses the legacy `{ content }` object form to exercise description normalization. +// them. One header uses the legacy `{ content }` object form to exercise description normalization, +// and one carries a multi-paragraph description to exercise line breaks in the docs tooltip. export const descriptionsFixtureCollection = { opencollection: '1.0.0', info: { name: 'Descriptions Demo', version: '1.0.0' }, @@ -22,6 +23,11 @@ export const descriptionsFixtureCollection = { name: 'X-Trace', value: 'abc-123', description: { content: 'Correlation id echoed back on the response', type: 'text' } + }, + { + name: 'X-Request-Id', + value: 'req-7', + description: 'Identifies this request across every service that handles it, from the gateway to the database.\n\nSend a fresh value per call so retries can be told apart in the logs.' } ], body: { diff --git a/packages/bruno-api-docs/src/e2eFixtures/qaCollection.ts b/packages/bruno-api-docs/src/e2eFixtures/qaCollection.ts index 8fc1e859..344aaf53 100644 --- a/packages/bruno-api-docs/src/e2eFixtures/qaCollection.ts +++ b/packages/bruno-api-docs/src/e2eFixtures/qaCollection.ts @@ -6,26 +6,30 @@ import type { OpenCollection } from '@opencollection/types'; * Built to be awkward on purpose: chains deep enough to elide, folders that * share a name across branches, names long enough to fight the row for width, * a folder whose own name contains the breadcrumb separator, and folders that - * count zero or one request. Not used by any automated spec, so it is safe to - * keep adding hostile cases here. + * count zero or one request. The collection name, the `gateway_query_key` variable + * and the Typography folder are full of descenders (g, j, p, q, y) to check that + * truncated text never clips them. Not used by any automated spec, so it is safe + * to keep adding hostile cases here. */ export const qaFixtureCollection = { opencollection: '1.0.0', - info: { name: 'QA Bench', version: '1.0.0' }, + info: { name: 'QA Bench: jumpy glyph typography', version: '1.0.0' }, config: { environments: [ { name: 'Dev', variables: [ { name: 'host', value: 'https://api.qa.dev' }, - { name: 'api_key', value: 'dev-key-123' } + { name: 'api_key', value: 'dev-key-123' }, + { name: 'gateway_query_key', value: 'jpgqy-gypsy-query-key' } ] }, { name: 'Prod', variables: [ { name: 'host', value: 'https://api.qa.com' }, - { name: 'api_key', value: 'prod-key-abc' } + { name: 'api_key', value: 'prod-key-abc' }, + { name: 'gateway_query_key', value: 'jpgqy-gypsy-query-key-prod' } ] } ] @@ -209,7 +213,22 @@ export const qaFixtureCollection = { } ] }, - { name: 'Health Check', type: 'http', seq: 5, method: 'GET', url: '{{host}}/ping' } + { name: 'Health Check', type: 'http', seq: 5, method: 'GET', url: '{{host}}/ping' }, + { + name: 'Typography', + type: 'folder', + seq: 6, + items: [ + { + name: 'Query gateway policy', + type: 'http', + seq: 1, + method: 'GET', + url: '{{host}}/gateway/jpgqy/policy?query={{gateway_query_key}}', + headers: [{ name: 'X-Gateway-Policy-Key', value: '{{gateway_query_key}}' }] + } + ] + } ] } as unknown as OpenCollection; diff --git a/packages/bruno-api-docs/src/pages/Environments/StyledWrapper.ts b/packages/bruno-api-docs/src/pages/Environments/StyledWrapper.ts index 1de58875..f81d43d5 100644 --- a/packages/bruno-api-docs/src/pages/Environments/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/pages/Environments/StyledWrapper.ts @@ -74,7 +74,8 @@ export const StyledWrapper = styled.div` line-height: 1; letter-spacing: 0; color: var(--oc-primary-strong); - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; } @@ -86,7 +87,8 @@ export const StyledWrapper = styled.div` line-height: 1; letter-spacing: 0; color: var(--text-primary); /* #343434 */ - overflow: hidden; + overflow-x: clip; + overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/bruno-api-docs/src/pages/GrpcRequest/GrpcRequest.tsx b/packages/bruno-api-docs/src/pages/GrpcRequest/GrpcRequest.tsx index 94a21af6..730c4e41 100644 --- a/packages/bruno-api-docs/src/pages/GrpcRequest/GrpcRequest.tsx +++ b/packages/bruno-api-docs/src/pages/GrpcRequest/GrpcRequest.tsx @@ -186,7 +186,10 @@ export const GrpcRequest: React.FC = ({ {name} - +
+ +
+ {descHtml && (
{ expect(html).toContain('Collection Configuration'); }); + it('shows no collection-level tags, which the desktop app does not support', () => { + const collection = { + info: { name: 'Tagged API', version: '1.0.0', tags: ['public'] } + } as unknown as OpenCollection; + + const html = renderToStaticMarkup(); + + expect(html).not.toContain('overview-tags'); + expect(html).not.toContain('public'); + }); + it('renders an empty-state placeholder for each section when the collection is bare', () => { const collection: OpenCollection = { info: { name: 'Empty API', version: '1.0.0' } diff --git a/packages/bruno-api-docs/src/pages/Overview/Overview.tsx b/packages/bruno-api-docs/src/pages/Overview/Overview.tsx index 351ae60c..2e78c4d3 100644 --- a/packages/bruno-api-docs/src/pages/Overview/Overview.tsx +++ b/packages/bruno-api-docs/src/pages/Overview/Overview.tsx @@ -3,7 +3,7 @@ import type { OpenCollection } from '@opencollection/types'; import type { StructuredText } from '@opencollection/types/common/description'; import { useMarkdownRenderer } from '@/hooks'; import { getCollectionStats, hasCollectionConfiguration } from '@/utils/collectionOverview'; -import { scriptsArrayToObject, getCollectionTags } from '@/utils/schemaHelpers'; +import { scriptsArrayToObject } from '@/utils/schemaHelpers'; import { getCollectionVariables } from '@/utils/request'; import { AUTH_MODE_LABELS } from '@/constants'; import { CollectionStats } from '../../components/CollectionStats/CollectionStats'; @@ -13,7 +13,6 @@ import { PageWrapper } from '../../components/PageWrapper/PageWrapper'; import { Heading } from '../../components/Heading/Heading'; import { Section } from '../../components/Section/Section'; import { ViewMore } from '../../components/ViewMore/ViewMore'; -import { Tags } from '@/components/Tags/Tags'; import { BookIcon } from '@/assets/icons'; import { StyledWrapper } from './StyledWrapper'; @@ -43,7 +42,6 @@ export const Overview: React.FC = ({ collection, testId = 'overvi const { preVars, postVars } = useMemo(() => getCollectionVariables(collection), [collection]); const version = collection.info?.version; const name = collection.info?.name || 'Untitled Collection'; - const tags = getCollectionTags(collection); const docsHtml = useMemo(() => { const content = getDocsContent(collection.docs); @@ -70,7 +68,6 @@ export const Overview: React.FC = ({ collection, testId = 'overvi
{`Version : ${version}`}
) : null} {name} - {tags.length > 0 && }
diff --git a/packages/bruno-api-docs/src/pages/Overview/StyledWrapper.ts b/packages/bruno-api-docs/src/pages/Overview/StyledWrapper.ts index 17931a13..cbdb8ce4 100644 --- a/packages/bruno-api-docs/src/pages/Overview/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/pages/Overview/StyledWrapper.ts @@ -17,9 +17,6 @@ export const StyledWrapper = styled.div` margin-bottom: 0.3rem; font-weight: 600; } - .overview-tags { - margin-top: 0.75rem; - } .overview-stats-row { margin-top: 1.25rem; } diff --git a/packages/bruno-api-docs/src/sampleCollection.ts b/packages/bruno-api-docs/src/sampleCollection.ts index ff255073..0aeb88f3 100644 --- a/packages/bruno-api-docs/src/sampleCollection.ts +++ b/packages/bruno-api-docs/src/sampleCollection.ts @@ -4,7 +4,6 @@ info: name: "Bruno Testbench" summary: "A comprehensive API collection for testing OpenCollection features" version: "1.0.0" - tags: ["testbench"] config: environments: - name: "Local" @@ -59,6 +58,15 @@ config: - name: apiKey secretName: prod/payment-gateway/api-key enabled: true + - name: "staging-regression-automation-eu-west-1" + color: "#2563eb" + variables: + - name: "host" + value: "https://staging-eu-west-1.echo.usebruno.com" + - name: "Customer Sandbox (Partner Integrations, Asia Pacific)" + variables: + - name: "host" + value: "https://sandbox-apac.echo.usebruno.com" request: headers: - name: "collection-header" @@ -2687,6 +2695,30 @@ items: data: | {} + - name: "user options" + type: "http" + seq: 12 + method: "OPTIONS" + url: "{{host}}/api/users" + headers: + - name: "Origin" + value: "https://docs.usebruno.com" + - name: "Access-Control-Request-Method" + value: "POST" + docs: "Ask the server which methods and headers the users endpoint accepts, the way a browser does before a cross-origin request." + examples: + - name: "Allowed" + response: + status: 204 + statusText: "No Content" + headers: + - name: "Allow" + value: "GET, POST, PUT, PATCH, DELETE, OPTIONS" + - name: "Access-Control-Allow-Origin" + value: "https://docs.usebruno.com" + - name: "Access-Control-Allow-Methods" + value: "GET, POST, PUT, PATCH, DELETE, OPTIONS" + - info: name: "GraphQL Fixtures" type: "folder" diff --git a/packages/bruno-api-docs/src/ui/CodeEditor/CodeEditor.tsx b/packages/bruno-api-docs/src/ui/CodeEditor/CodeEditor.tsx index 6c377a15..6066a0a7 100644 --- a/packages/bruno-api-docs/src/ui/CodeEditor/CodeEditor.tsx +++ b/packages/bruno-api-docs/src/ui/CodeEditor/CodeEditor.tsx @@ -7,8 +7,9 @@ import { Portal } from '@/ui/Portal/Portal'; import { ensureScriptApiCompletions, setModelHints } from './scriptApiCompletions'; import { createVariableDecorator, type VariableDecorator } from './variableDecorations'; import { createVariableHover, type VariableHover } from './variableHoverWidget'; +import { adoptContextView } from './contextViewPortal'; import type { ScriptApiRoot } from '@/utils/scriptAutocomplete'; -import { StyledWrapper } from './StyledWrapper'; +import { ContextViewHost, StyledWrapper } from './StyledWrapper'; type EditorInstance = Parameters[0]; @@ -71,6 +72,8 @@ const CodeEditor: React.FC = ({ const hoverRef = useRef(null); const isFoundRef = useRef(resolver.isFound); const [hoveredVariable, setHoveredVariable] = useState<{ name: string; node: HTMLElement } | null>(null); + const [editorContainer, setEditorContainer] = useState(null); + const [contextViewHost, setContextViewHost] = useState(null); useEffect(() => { if (active && editorRef.current) editorRef.current.layout(); @@ -85,6 +88,11 @@ const CodeEditor: React.FC = ({ decoratorRef.current?.refresh(); }, [resolver.activeEnvName, variableSignature]); + useEffect(() => { + if (!editorContainer || !contextViewHost) return undefined; + return adoptContextView(editorContainer, contextViewHost); + }, [editorContainer, contextViewHost]); + useEffect( () => () => { decoratorRef.current?.dispose(); @@ -97,6 +105,7 @@ const CodeEditor: React.FC = ({ const handleMount: OnMount = (editor, monaco) => { editorRef.current = editor; + setEditorContainer(editor.getContainerDomNode()); if (variableAware) { decoratorRef.current = createVariableDecorator(editor, monaco, () => isFoundRef.current); @@ -193,6 +202,9 @@ const CodeEditor: React.FC = ({ testId={testId ? `${testId}-copy` : undefined} /> ) : null} + + + {hoveredVariable && renderVariableCard ? ( {renderVariableCard(hoveredVariable.name)} diff --git a/packages/bruno-api-docs/src/ui/CodeEditor/StyledWrapper.ts b/packages/bruno-api-docs/src/ui/CodeEditor/StyledWrapper.ts index 79c1b923..91121004 100644 --- a/packages/bruno-api-docs/src/ui/CodeEditor/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/ui/CodeEditor/StyledWrapper.ts @@ -33,3 +33,15 @@ export const StyledWrapper = styled.div` color: var(--oc-codemirror-variable-prompt) !important; } `; + +export const ContextViewHost = styled.div` + position: fixed; + z-index: var(--z-popover); + width: 100vw; + height: 0; + pointer-events: none; + + .context-view { + pointer-events: auto; + } +`; diff --git a/packages/bruno-api-docs/src/ui/CodeEditor/contextViewPortal.ts b/packages/bruno-api-docs/src/ui/CodeEditor/contextViewPortal.ts new file mode 100644 index 00000000..17edcfe4 --- /dev/null +++ b/packages/bruno-api-docs/src/ui/CodeEditor/contextViewPortal.ts @@ -0,0 +1,31 @@ +const CONTEXT_VIEW_CLASS = 'context-view'; + +// Moves Monaco's tooltip layer out of the editor container into `host`, a fixed layer aligned with +// the container, so its container-relative position stays right while no ancestor can clip it. +export const adoptContextView = (container: HTMLElement, host: HTMLElement): (() => void) => { + const alignHost = () => { + const rect = container.getBoundingClientRect(); + host.style.top = `${rect.top}px`; + host.style.left = `${rect.left}px`; + }; + + const viewObserver = new MutationObserver(alignHost); + + const adopt = (node: Node) => { + if (!(node instanceof HTMLElement) || !node.classList.contains(CONTEXT_VIEW_CLASS)) return; + alignHost(); + host.appendChild(node); + viewObserver.observe(node, { attributes: true, attributeFilter: ['style'] }); + }; + + const containerObserver = new MutationObserver((records) => { + records.forEach((record) => record.addedNodes.forEach(adopt)); + }); + containerObserver.observe(container, { childList: true }); + Array.from(container.children).forEach(adopt); + + return () => { + containerObserver.disconnect(); + viewObserver.disconnect(); + }; +}; diff --git a/packages/bruno-api-docs/src/ui/Tooltip/StyledWrapper.ts b/packages/bruno-api-docs/src/ui/Tooltip/StyledWrapper.ts index 298b8587..537a8e1b 100644 --- a/packages/bruno-api-docs/src/ui/Tooltip/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/ui/Tooltip/StyledWrapper.ts @@ -4,15 +4,20 @@ export const StyledWrapper = styled.div` z-index: var(--z-popover, 1000); max-width: min(24rem, calc(100vw - 1rem)); padding: 0.25rem 0.5rem; - border-radius: var(--oc-radius); - background: var(--text-primary); - color: var(--oc-background-base); + border-radius: 0.25rem; + border: 1px solid var(--oc-border-border2); + background: var(--oc-background-surface0); + color: var(--oc-text); font-family: var(--font-sans); font-size: 0.75rem; font-weight: 400; line-height: 1.35; white-space: normal; overflow-wrap: anywhere; - box-shadow: 0 4px 12px var(--oc-info-tip-box-shadow); + box-shadow: 0 2px 6px var(--oc-info-tip-box-shadow); pointer-events: none; + + &.oc-tooltip--multiline { + white-space: pre-line; + } `; diff --git a/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx b/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx index 377dcb04..98ad92d4 100644 --- a/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx +++ b/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx @@ -22,6 +22,7 @@ interface TooltipProps { * Hover dwell before opening, in ms; defaults to opening immediately. */ openDelay?: number; + multiline?: boolean; className?: string; testId?: string; } @@ -59,6 +60,7 @@ export const Tooltip: React.FC = ({ shouldOpen, touch = true, openDelay = 0, + multiline = false, className, testId = 'tooltip' }) => { @@ -196,7 +198,7 @@ export const Tooltip: React.FC = ({ ref={bubbleRef} aria-hidden="true" data-testid={testId} - className={['oc-tooltip', className].filter(Boolean).join(' ')} + className={['oc-tooltip', multiline && 'oc-tooltip--multiline', className].filter(Boolean).join(' ')} style={{ position: 'fixed', top: pos ? pos.top : -9999, diff --git a/packages/bruno-api-docs/src/utils/schemaHelpers.spec.ts b/packages/bruno-api-docs/src/utils/schemaHelpers.spec.ts index b0798ec0..9845d95c 100644 --- a/packages/bruno-api-docs/src/utils/schemaHelpers.spec.ts +++ b/packages/bruno-api-docs/src/utils/schemaHelpers.spec.ts @@ -19,7 +19,6 @@ import { getGrpcMetadata, getGrpcProtoFileName, getItemTags, - getCollectionTags, getInheritedTags, type RequestItem } from './schemaHelpers'; @@ -28,8 +27,6 @@ const item = (data: Record): OpenCollectionItem => data as unkn const requestItem = (data: Record): RequestItem => data as unknown as RequestItem; -const collection = (data: Record) => data as unknown as Parameters[0]; - describe('getItemDescription', () => { it('reads a plain string description from the info block', () => { expect(getItemDescription({ info: { description: 'Short summary.' } } as any)).toBe('Short summary.'); @@ -351,17 +348,6 @@ describe('getInheritedTags', () => { }); }); -describe('getCollectionTags', () => { - it('reads tags from the collection info block', () => { - expect(getCollectionTags(collection({ info: { name: 'Hotel Booking', tags: ['public'] } }))).toEqual(['public']); - }); - - it('returns an empty array when absent', () => { - expect(getCollectionTags(null)).toEqual([]); - expect(getCollectionTags(collection({ info: { name: 'Hotel Booking' } }))).toEqual([]); - }); -}); - describe('countEnabled', () => { it('counts only the rows the user has ticked', () => { expect(countEnabled([{ disabled: false }, { disabled: true }])).toBe(1); diff --git a/packages/bruno-api-docs/src/utils/schemaHelpers.ts b/packages/bruno-api-docs/src/utils/schemaHelpers.ts index 39f314f9..ddc7d190 100644 --- a/packages/bruno-api-docs/src/utils/schemaHelpers.ts +++ b/packages/bruno-api-docs/src/utils/schemaHelpers.ts @@ -109,10 +109,6 @@ export const getItemTags = (item: OpenCollectionItem | null | undefined): string return []; }; -/** Get the collection-level tags from the info block. */ -export const getCollectionTags = (collection: OpenCollection | null | undefined): string[] => - normalizeTags((collection as { info?: { tags?: string[] } } | null | undefined)?.info?.tags); - /** Tags carried by ancestor folders that the item does not carry itself. */ export const getInheritedTags = (ancestry: OpenCollectionItem[], ownTags: string[]): string[] => { const own = new Set(ownTags); From 47394347d6088ae6e0a5812a3f0e0589d17acaa4 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 24 Sep 2026 01:27:18 +0530 Subject: [PATCH 2/5] comments addressed --- .../src/components/Search/SearchResultItem/StyledWrapper.ts | 2 +- .../src/components/ShowVarsToggle/ShowVarsToggle.tsx | 1 - .../src/components/ThemeToggle/ThemeToggle.tsx | 2 +- packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx | 5 ++--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts b/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts index 68dc61b2..0b97ea93 100644 --- a/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts +++ b/packages/bruno-api-docs/src/components/Search/SearchResultItem/StyledWrapper.ts @@ -25,7 +25,7 @@ export const StyledWrapper = styled.button` .search-result-method { flex-shrink: 0; display: inline-block; - min-width: 26px; + min-width: 1.625rem; margin-top: 1px; text-align: left; font-family: var(--font-mono); diff --git a/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx b/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx index 6b90f180..c7cb5460 100644 --- a/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx +++ b/packages/bruno-api-docs/src/components/ShowVarsToggle/ShowVarsToggle.tsx @@ -20,7 +20,6 @@ const ShowVarsToggle: React.FC = ({ testId = 'show-vars-tog return ( = ({ testId = 'theme-toggle' }) return ( - diff --git a/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx b/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx index 98ad92d4..3cd32615 100644 --- a/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx +++ b/packages/bruno-api-docs/src/ui/Tooltip/Tooltip.tsx @@ -10,6 +10,7 @@ import React, { } from 'react'; import { Portal } from '@/ui/Portal/Portal'; import { GAP, VIEWPORT_MARGIN } from '@/constants/ui'; +import cx from '@/utils/cx'; import { StyledWrapper } from './StyledWrapper'; interface TooltipProps { @@ -118,8 +119,6 @@ export const Tooltip: React.FC = ({ [disabled, shouldOpen, cancelPendingOpen] ); - // Leaving before the dwell elapses must also drop the pending open, or the - // bubble appears over whatever the pointer moved on to. const hide = useCallback(() => { cancelPendingOpen(); setOpen(false); @@ -198,7 +197,7 @@ export const Tooltip: React.FC = ({ ref={bubbleRef} aria-hidden="true" data-testid={testId} - className={['oc-tooltip', multiline && 'oc-tooltip--multiline', className].filter(Boolean).join(' ')} + className={cx('oc-tooltip', multiline && 'oc-tooltip--multiline', className)} style={{ position: 'fixed', top: pos ? pos.top : -9999, From b9540606a5513a147a174672f11983474c6233d3 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 24 Sep 2026 11:22:11 +0530 Subject: [PATCH 3/5] session storage keys prefixed --- .../e2e/pages/grpc-request.page.ts | 1 + .../e2e/tests/app/storage-keys.spec.ts | 30 +++++++++++++++++++ .../RequestPane/RequestPane.tsx | 2 +- .../ResponsePane/ResponsePane.tsx | 2 +- .../src/components/Section/Section.tsx | 2 +- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 packages/bruno-api-docs/e2e/tests/app/storage-keys.spec.ts diff --git a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts index 1a1bc930..e3ccadd1 100644 --- a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts +++ b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts @@ -39,6 +39,7 @@ export class GrpcRequestPage extends BasePage { readonly codeSnippet: Locator = this.snippet.code; readonly executionContextSection: Locator = this.page.getByTestId('grpc-request-section-execution-context'); + readonly executionContextToggle: Locator = this.executionContextSection.getByRole('button', { name: 'Execution Context' }); readonly executionContextEmpty: Locator = this.page.getByTestId('grpc-request-execution-context-empty'); async open(path: string[]): Promise { diff --git a/packages/bruno-api-docs/e2e/tests/app/storage-keys.spec.ts b/packages/bruno-api-docs/e2e/tests/app/storage-keys.spec.ts new file mode 100644 index 00000000..24e0e441 --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/app/storage-keys.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../../playwright'; + +const USERS_BODY = '{"data":[{"id":1,"name":"Alice"}]}'; + +test.describe('Browser storage keys', () => { + test('everything the app remembers is stored under the oc-docs prefix', async ({ + page, + grpcRequestPage, + playground, + responsePane + }) => { + await grpcRequestPage.open(['Realtime', 'Order Service']); + await grpcRequestPage.executionContextToggle.click(); + + await responsePane.mockUsersResponse(USERS_BODY); + await playground.open('bottom'); + await playground.openRequest('get users'); + await playground.selectTab('headers'); + await responsePane.send(); + await responsePane.switchToTab('headers'); + + const keys = await page.evaluate(() => [...Object.keys(sessionStorage), ...Object.keys(localStorage)]); + expect(keys).toEqual(expect.arrayContaining([ + 'oc-docs:playgroundRequestTab', + 'oc-docs:playgroundResponseTab', + 'oc-docs:section-grpc-request-execution-context' + ])); + expect(keys.filter((key) => !key.startsWith('oc-docs'))).toEqual([]); + }); +}); diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/RequestPane/RequestPane.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/RequestPane/RequestPane.tsx index ad44c18b..3c73a0ee 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/RequestPane/RequestPane.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/RequestPane/RequestPane.tsx @@ -41,7 +41,7 @@ interface RequestPaneProps { } const RequestPane: React.FC = ({ item, onItemChange, inheritedAuth }) => { - const [activeTab, setActiveTab] = useSessionStorage('playground-request-tab', 'overview'); + const [activeTab, setActiveTab] = useSessionStorage('oc-docs:playgroundRequestTab', 'overview'); const handleParamsChange = (params: KeyValueRow[]) => { const updatedParams = params.map((p) => ({ ...keyValueRowToEntry(p), type: p.type })); diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/ResponsePane/ResponsePane.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/ResponsePane/ResponsePane.tsx index 609a3381..86f6ef25 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/ResponsePane/ResponsePane.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/ResponsePane/ResponsePane.tsx @@ -27,7 +27,7 @@ interface ResponsePaneProps { } const ResponsePane: React.FC = ({ response, isLoading, orientation, itemUuid }) => { - const [activeTab, setActiveTab] = useSessionStorage('playground-response-tab', 'response'); + const [activeTab, setActiveTab] = useSessionStorage('oc-docs:playgroundResponseTab', 'response'); const [dismissedScriptErrorKeys, setDismissedScriptErrorKeys] = useState([]); const { actionsExpandedWidth, measureActions } = useResponseActions(); diff --git a/packages/bruno-api-docs/src/components/Section/Section.tsx b/packages/bruno-api-docs/src/components/Section/Section.tsx index a6f36b36..efef6bf2 100644 --- a/packages/bruno-api-docs/src/components/Section/Section.tsx +++ b/packages/bruno-api-docs/src/components/Section/Section.tsx @@ -39,7 +39,7 @@ export const Section: React.FC = ({ navGroup, labelClassName }) => { - const [open, setOpen] = useSessionStorage(storageKey ? `section-${storageKey}` : '', defaultOpen); + const [open, setOpen] = useSessionStorage(storageKey ? `oc-docs:section-${storageKey}` : '', defaultOpen); const panelId = useId(); const labelId = useId(); From 3d1f061b457b1627f6e3dd34d63360ab3a73f859 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 24 Sep 2026 15:48:12 +0530 Subject: [PATCH 4/5] comments addressed --- packages/bruno-api-docs/e2e/pages/grpc-request.page.ts | 2 +- .../src/components/Section/Section.spec.tsx | 9 +++++++++ .../bruno-api-docs/src/components/Section/Section.tsx | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts index e3ccadd1..ff553f79 100644 --- a/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts +++ b/packages/bruno-api-docs/e2e/pages/grpc-request.page.ts @@ -39,7 +39,7 @@ export class GrpcRequestPage extends BasePage { readonly codeSnippet: Locator = this.snippet.code; readonly executionContextSection: Locator = this.page.getByTestId('grpc-request-section-execution-context'); - readonly executionContextToggle: Locator = this.executionContextSection.getByRole('button', { name: 'Execution Context' }); + readonly executionContextToggle: Locator = this.page.getByTestId('grpc-request-section-execution-context-toggle'); readonly executionContextEmpty: Locator = this.page.getByTestId('grpc-request-execution-context-empty'); async open(path: string[]): Promise { diff --git a/packages/bruno-api-docs/src/components/Section/Section.spec.tsx b/packages/bruno-api-docs/src/components/Section/Section.spec.tsx index 9b28debd..c6d9d664 100644 --- a/packages/bruno-api-docs/src/components/Section/Section.spec.tsx +++ b/packages/bruno-api-docs/src/components/Section/Section.spec.tsx @@ -13,4 +13,13 @@ describe('Section', () => { expect(html).toContain('Environments'); expect(html).toContain('List content'); }); + + it('gives the collapse toggle a test id derived from the section test id', () => { + const html = renderToStaticMarkup( +
+

Scripts

+
+ ); + expect(html).toContain('data-testid="request-section-execution-context-toggle"'); + }); }); diff --git a/packages/bruno-api-docs/src/components/Section/Section.tsx b/packages/bruno-api-docs/src/components/Section/Section.tsx index efef6bf2..d44d8587 100644 --- a/packages/bruno-api-docs/src/components/Section/Section.tsx +++ b/packages/bruno-api-docs/src/components/Section/Section.tsx @@ -61,6 +61,7 @@ export const Section: React.FC = ({ type="button" id={labelId} className="section-toggle" + data-testid={testId ? `${testId}-toggle` : undefined} aria-expanded={open} aria-controls={panelId} onClick={() => setOpen((v) => !v)} From c300216f1e1c5ec68d8fa0135bf4cfd4bcf7ab5b Mon Sep 17 00:00:00 2001 From: Arpit Date: Thu, 24 Sep 2026 22:32:31 +0530 Subject: [PATCH 5/5] Fix UI bugs and improve documentation clarity --- .changeset/many-buckets-shout.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/many-buckets-shout.md diff --git a/.changeset/many-buckets-shout.md b/.changeset/many-buckets-shout.md new file mode 100644 index 00000000..3e84b685 --- /dev/null +++ b/.changeset/many-buckets-shout.md @@ -0,0 +1,13 @@ +--- +"@usebruno/api-docs": patch +--- + +fix(docs): parity UI bugs 2 +16: On the gRPC request page, the URL bar now stays at the top while you scroll, like the other request pages. +20: The "Show vars" switch now shows a tooltip. +22: In the script editor's search box, the button tooltips no longer get cut off or flicker. +27: In example cards, the scrollbar now only shows when you hover over the content. +28: Letters like g, y and p are no longer cut off at the bottom in the collection name, variable card, environments page, table keys and search results. +30: In search results, method names like POST and PATCH are shown in full instead of being cut off with "...". +35: Tooltips now match the light and dark theme instead of being the opposite colour. +36: Description tooltips now keep the line breaks and blank lines written in the Bruno app.