Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eda0428
Improve the small size menu dropdown
vasharma05-bruno Jul 30, 2026
e646ea1
Fix the text preview color in Dock mode
vasharma05-bruno Jul 30, 2026
f40fdb5
Move selected response format and response preview to redux store
vasharma05-bruno Jul 30, 2026
9a54d0d
Improve the copy functionality, along with adding icons to Large Warn…
vasharma05-bruno Jul 30, 2026
99210f4
Replace ActionIcon with IconButton
vasharma05-bruno Jul 30, 2026
bd9cdd7
Remove the comments from ResponseFormatter.tsx
vasharma05-bruno Jul 30, 2026
75ce91c
Remove the comments from ResponseFormatter.tsx
vasharma05-bruno Jul 30, 2026
d47790f
Fix the regression in menudropdown for response formatter and action …
vasharma05-bruno Jul 31, 2026
f625fb9
Fix the buggy preview toggle
vasharma05-bruno Jul 31, 2026
50f56f3
Clean up
vasharma05-bruno Jul 31, 2026
f5685a4
Add missing dependencies
vasharma05-bruno Jul 31, 2026
5fb6538
Display tooltips in Icon buttons
vasharma05-bruno Jul 31, 2026
90834e6
Hide the copy button in response body editor
vasharma05-bruno Jul 31, 2026
78777e0
Add icon to response formatter trigger
vasharma05-bruno Jul 31, 2026
d752b83
Add missing e2e tests for response actions and response body
vasharma05-bruno Jul 31, 2026
3e8d2b2
Display status text in right panel of response tabs
vasharma05-bruno Jul 31, 2026
9110d92
Fix response size display
vasharma05-bruno Jul 31, 2026
be29e87
Keep PDF keep only the width of page
vasharma05-bruno Jul 31, 2026
5029e3f
Remove unnecessary comment
vasharma05-bruno Jul 31, 2026
fa3eae9
Update the icon size and stroke width for response actions
vasharma05-bruno Jul 31, 2026
f4c6cff
Fix the icon color and stroke width for response actions
vasharma05-bruno Jul 31, 2026
0c8871b
Remove delay in displaying tooltip on IconButton
vasharma05-bruno Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class ResponsePaneComponent extends BaseComponent {
readonly bodyEditor = new CodeEditorComponent(this.page, 'response-body-editor');
readonly sendButton = this.page.getByTestId('query-bar-send');
readonly formatSelector = this.page.getByTestId('response-format-selector');
// The leading icon in the selector trigger (the eye when preview is on, else the format's icon).
readonly formatSelectorIcon = this.page.getByTestId('response-format-selector-trigger-icon');

// The response actions live in the tab bar's right slot and only render once a response exists
// (and there is no request error). They are responsive: collapsed into a kebab menu when the slot
Expand Down Expand Up @@ -87,7 +89,9 @@ export class ResponsePaneComponent extends BaseComponent {
await expect(inlineTab).toHaveAttribute('aria-selected', 'true');
}

/** The inline Change-Layout button, shown when the actions are expanded (wide pane). */
readonly inlineCopyButton = this.inlineButtons.getByRole('button', { name: 'Copy Response' });
readonly inlineDownloadButton = this.inlineButtons.getByRole('button', { name: 'Download Response' });
readonly inlineClearButton = this.inlineButtons.getByRole('button', { name: 'Clear Response' });
readonly inlineChangeLayoutButton = this.inlineButtons.getByRole('button', { name: 'Change Layout' });

async openFormatSelector(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Page } from '@playwright/test';
import { test, expect } from '../../playwright';

// The response-pane actions (Copy, Download, Clear, Change Layout) only render once a response is
Expand All @@ -6,6 +7,17 @@ import { test, expect } from '../../playwright';
// host = http://localhost:8081 in the Local env) rather than a proxy.
const USERS_BODY = '{"data":[{"id":1,"name":"Alice"}]}';

// Read and parse the clipboard, returning null while it is still empty/unparseable so `expect.poll`
// can retry until the async copy lands.
const readClipboardJson = (page: Page): Promise<unknown> =>
page.evaluate(async () => {
try {
return JSON.parse(await navigator.clipboard.readText());
} catch {
return null;
}
});

// At the default (narrow) playground width the actions collapse into a kebab menu; each action is a
// menu item.
test.describe('response pane actions — collapsed', () => {
Expand Down Expand Up @@ -36,6 +48,16 @@ test.describe('response pane actions — collapsed', () => {
await expect(responsePane.actions).toBeVisible();
});

test('Copy writes the response body to the clipboard', async ({ page, responsePane }) => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);

await responsePane.openActionsMenu();
await responsePane.copyMenuItem.click();

// The copied text is pretty-printed JSON, so compare the parsed value rather than the raw string.
await expect.poll(() => readClipboardJson(page)).toEqual(JSON.parse(USERS_BODY));
});

test('Clear returns the pane to the empty "Click Send" state', async ({ responsePane }) => {
await responsePane.openActionsMenu();
await responsePane.clearMenuItem.click();
Expand All @@ -50,7 +72,8 @@ test.describe('response pane actions — collapsed', () => {
await responsePane.openActionsMenu();
await responsePane.downloadMenuItem.click();
const download = await downloadPromise;
expect(download.suggestedFilename().length).toBeGreaterThan(0);
// JSON body with no Content-Disposition and a `/api/users` path → the content-type extension.
expect(download.suggestedFilename()).toBe('response.json');
});
});

Expand All @@ -70,7 +93,32 @@ test.describe('response pane actions — expanded', () => {
test('shows the actions as inline buttons instead of the kebab menu', async ({ responsePane }) => {
await expect(responsePane.inlineButtons).toBeVisible();
await expect(responsePane.actionsMenuTrigger).toBeHidden();
await expect(responsePane.inlineButtons.getByRole('button', { name: 'Copy Response' })).toBeVisible();
await expect(responsePane.inlineCopyButton).toBeVisible();
});

test('Copy writes the response body to the clipboard from the inline button', async ({
page,
responsePane
}) => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);

await responsePane.inlineCopyButton.click();

await expect.poll(() => readClipboardJson(page)).toEqual(JSON.parse(USERS_BODY));
});

test('Download triggers a browser download from the inline button', async ({ page, responsePane }) => {
const downloadPromise = page.waitForEvent('download');
await responsePane.inlineDownloadButton.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('response.json');
});

test('Clear returns the pane to the empty state from the inline button', async ({ responsePane }) => {
await responsePane.inlineClearButton.click();

await expect(responsePane.emptyHint).toBeVisible();
await expect(responsePane.actions).toHaveCount(0);
});

test('Change Layout toggles the container orientation horizontal <-> vertical', async ({
Expand Down
29 changes: 29 additions & 0 deletions packages/bruno-api-docs/e2e/tests/playground/response-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ test.describe('response body — editor formatting per format', () => {
await responsePane.selectFormat('base64');
await expect.poll(() => responsePane.bodyText()).toBe(BASE64_BODY);
});

test('the selector trigger shows the format icon, swapping to the eye icon in preview', async ({
page,
playground,
responsePane
}) => {
await page.goto('/#/?pg=1&dock=bottom');
await playground.openSidebarItem('get users');
await responsePane.send();

// JSON default (preview off): the trigger carries the format's own icon (braces).
await expect(responsePane.formatSelectorIcon).toHaveClass(/icon-tabler-braces/);

// Turning preview on swaps the trigger icon to the eye.
await responsePane.openFormatSelector();
await responsePane.togglePreview();
expect(await responsePane.isPreviewOn()).toBe(true);
await expect(responsePane.formatSelectorIcon).toHaveClass(/icon-tabler-eye/);

// Turning it back off restores the format icon.
await responsePane.togglePreview();
expect(await responsePane.isPreviewOn()).toBe(false);
await expect(responsePane.formatSelectorIcon).toHaveClass(/icon-tabler-braces/);

// Changing the format updates the trigger icon accordingly (raw -> file-text). The dropdown is
// still open from toggling preview, so pick the option directly.
await responsePane.formatOption('raw').click();
await expect(responsePane.formatSelectorIcon).toHaveClass(/icon-tabler-file-text/);
});
});

// A minimal 1x1 PNG — the magic-number sniffer classifies it as a byte format, so the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useCallback } from 'react';
import { formatBytes } from '@/utils/exampleResponse';
import CopyButton from '@/ui/CopyButton/CopyButton';
import type { RunRequestResponse } from '@/runner';
import { downloadResponse } from '@/utils/downloadResponse';
import { StyledWrapper } from './StyledWrapper';
import { IconAlertTriangle } from '@tabler/icons';
import { IconAlertTriangle, IconCheck, IconCopy, IconDownload, IconEye } from '@tabler/icons';
import useCopy from '@/hooks/useCopy';

const LARGE_RESPONSE_THRESHOLD = 10 * 1024 * 1024; // 10 MB

Expand All @@ -20,6 +20,10 @@ export const LargeResponseWarning: React.FC<LargeResponseWarningProps> = ({ resp
return typeof data === 'string' ? data : JSON.stringify(data ?? '', null, 2);
}, [response?.data]);

const { copied, copyResponse } = useCopy({
getText: dataToCopy
});

return (
<StyledWrapper data-testid="large-response-warning">
<div className="warning-icon">
Expand All @@ -41,14 +45,23 @@ export const LargeResponseWarning: React.FC<LargeResponseWarningProps> = ({ resp
aria-label="View response"
data-testid="large-response-view"
>
View
<span className="button-icon">
<IconEye size={13} strokeWidth={1} />
</span>
<span>View</span>
</button>
<button
type="button"
className="large-response-copy"
onClick={copyResponse}
aria-label={copied ? 'Copied response' : 'Copy response'}
data-testid="large-response-copy"
>
<span className="button-icon">
{copied ? <IconCheck size={13} strokeWidth={1} /> : <IconCopy size={13} strokeWidth={1} />}
</span>
<span>{copied ? 'Copied' : 'Copy'}</span>
</button>
<CopyButton
getText={dataToCopy}
label="Copy"
copiedLabel="Copied"
testId="large-response-copy"
/>
<button
type="button"
className="large-response-download"
Expand All @@ -57,7 +70,10 @@ export const LargeResponseWarning: React.FC<LargeResponseWarningProps> = ({ resp
aria-label="Download response"
data-testid="large-response-download"
>
Download
<span className="button-icon">
<IconDownload size={13} strokeWidth={1} />
</span>
<span>Download</span>
</button>
</div>
</StyledWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const StyledWrapper = styled.div`
}

.large-response-title {
color: var(--oc-status-warning-text);
font-weight: 600;
font-size: var(--oc-font-size-lg);
}
Expand All @@ -43,6 +42,7 @@ export const StyledWrapper = styled.div`
}

.large-response-view,
.large-response-copy,
.large-response-download {
display: inline-flex;
align-items: center;
Expand All @@ -55,6 +55,7 @@ export const StyledWrapper = styled.div`
font-size: var(--oc-font-size-base);
cursor: pointer;
transition: color 0.15s ease, background-color 0.15s ease;
gap: 0.5rem;

&:hover {
background-color: var(--badge-bg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const ResponseBodyTab: React.FC<ResponseBodyTabProps> = ({ response, selectedFor
height="100%"
readOnly={true}
testId="response-body-editor"
showCopy={false}
/>
</React.Suspense>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ActionIcon from '@/ui/ActionIcon/ActionIcon';
import IconButton from '@/ui/IconButton/IconButton';
import { IconLayoutColumns, IconLayoutRows } from '@tabler/icons';

interface ChangeLayoutProps {
Expand All @@ -9,13 +9,13 @@ interface ChangeLayoutProps {

const ChangeLayout: React.FC<ChangeLayoutProps> = ({ orientation, handleChangeLayout }) => {
return (
<ActionIcon label="Change Layout" className="p-1" onClick={handleChangeLayout}>
<IconButton label="Change Layout" className="p-1" onClick={handleChangeLayout}>
{orientation === 'vertical' ? (
<IconLayoutColumns size={16} strokeWidth={2} />
<IconLayoutColumns size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} />
) : (
<IconLayoutRows size={16} strokeWidth={2} />
<IconLayoutRows size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} />
)}
</ActionIcon>
</IconButton>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ActionIcon from '@/ui/ActionIcon/ActionIcon';
import IconButton from '@/ui/IconButton/IconButton';
import { IconEraser } from '@tabler/icons';

interface ClearResponseProps {
Expand All @@ -8,9 +8,9 @@ interface ClearResponseProps {

const ClearResponse: React.FC<ClearResponseProps> = ({ onClick }) => {
return (
<ActionIcon label="Clear Response" className="p-1" onClick={onClick}>
<IconEraser size={16} stroke={2} />
</ActionIcon>
<IconButton label="Clear Response" className="p-1" onClick={onClick}>
<IconEraser size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} />
</IconButton>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { IconCheck, IconCopy } from '@tabler/icons';
import ActionIcon from '@/ui/ActionIcon/ActionIcon';
import IconButton from '@/ui/IconButton/IconButton';

interface CopyResponseProps {
copied: boolean;
Expand All @@ -10,9 +10,9 @@ interface CopyResponseProps {

const CopyResponse: React.FC<CopyResponseProps> = ({ copied, onClick, disabled }) => {
return (
<ActionIcon label="Copy Response" className="p-1" disabled={disabled} onClick={onClick}>
{copied ? <IconCheck size={16} stroke={2} /> : <IconCopy size={16} stroke={2} />}
</ActionIcon>
<IconButton label="Copy Response" className="p-1" disabled={disabled} onClick={onClick}>
{copied ? <IconCheck size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} /> : <IconCopy size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} />}
</IconButton>
);
};

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ActionIcon from '@/ui/ActionIcon/ActionIcon';
import IconButton from '@/ui/IconButton/IconButton';
import { IconDownload } from '@tabler/icons';

interface DownloadResponseProps {
Expand All @@ -9,9 +9,9 @@ interface DownloadResponseProps {

const DownloadResponse: React.FC<DownloadResponseProps> = ({ onClick, disabled }) => {
return (
<ActionIcon label="Download Response" className="p-1" disabled={disabled} onClick={onClick}>
<IconDownload size={16} stroke={2} />
</ActionIcon>
<IconButton label="Download Response" className="p-1" disabled={disabled} onClick={onClick}>
<IconDownload size={13} stroke={1.5} style={{ color: 'var(--text-muted)' }} />
</IconButton>
);
};

Expand Down
Loading
Loading