Skip to content

feat: Add pretty print toggle for request and response bodies in network details#39682

Open
cpAdm wants to merge 1 commit intomicrosoft:mainfrom
cpAdm:feat-button-to-format-network-bodies
Open

feat: Add pretty print toggle for request and response bodies in network details#39682
cpAdm wants to merge 1 commit intomicrosoft:mainfrom
cpAdm:feat-button-to-format-network-bodies

Conversation

@cpAdm
Copy link
Contributor

@cpAdm cpAdm commented Mar 14, 2026

Follow up on #39405,

  • Add a button to toggle formatting for:
    • Request bodies (button in section title)
    • Response bodies (button in newly added bottom toolbar)
  • If the formatting fails, we show an error badge with title on the format button
    • Copied from uiModeView.tsx (extracted to ToolbarButton)
  • Remember setting option (default true) in local storage
  • Got rid of Loading... which in most cases just causing flickering anyway (since most bodies are fetched very quickly)
  • Previously, empty bodies showed <Empty>, but to line up with e.g. Chrome Devtools, we now just show an empty editor to avoid confusion
  • Previously, copying request would copy formatted request body, now it instead uses the original request body
Section On Off
Request body image image
Response body image image

If formatting fails:
image

Example test to showcase this new feature

it('Request API examples: JSON and XML echo', async ({ request, server }) => {
  // JSON example
  const jsonPayload = { message: 'hello from Playwright', id: 123 };
  const jsonResp = await request.post('https://postman-echo.com/post', { data: jsonPayload });
  expect(jsonResp.ok()).toBeTruthy();

  // XML example
  const xmlPayload = `
      <note>        <to>User</to>
        <from>Playwright</from>        <heading>Reminder</heading>
        <body>Hello XML world</body>      </note>`;
  const xmlResp = await request.post('https://postman-echo.com/post', {
    headers: { 'Content-Type': 'application/xml' },
    data: xmlPayload.trim(),
  });
  expect(xmlResp.ok()).toBeTruthy();

  // Malformed XML example (unclosed <body>)
  const malformedXmlPayload = `
    <note>
      <to>User</to>
      <from>Playwright</from>
      <heading>Broken</heading>
      <body>Hello XML world</body`;
  const malformedResp = await request.post('https://postman-echo.com/post', {
    headers: { 'Content-Type': 'application/xml' },
    data: malformedXmlPayload.trim(),
  });
  // postman-echo returns 200 and echoes the body even if malformed; adjust assertion if you switch to a stricter endpoint
  expect(malformedResp.ok()).toBeTruthy();

  // Malformed JSON response example: valid JSON request body, but response body is bad JSON
  server.setRoute('/malformed-json', (req, res) => {
    res.statusCode = 200;
    res.setHeader('content-type', 'application/json');
    res.end('{"foo": "bar"'); // missing closing brace -> malformed JSON
  });

  const validJsonPayload = { ping: 'pong' };
  const malformedJsonResp = await request.post(`${server.PREFIX}/malformed-json`, { data: validJsonPayload });
  expect(malformedJsonResp.ok()).toBeTruthy();
});

If we want, we can extract useFormattedBody and its helper functions to a separate file as suggested here?

Closes: #37963

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a user-facing “Pretty print” toggle in Trace Viewer’s Network details to switch between raw and formatted request/response bodies, with persisted settings and an error indicator when formatting fails.

Changes:

  • Introduces pretty-print toggles for request payload (header button) and response body (bottom toolbar) with localStorage-backed persistence.
  • Refactors the existing “error dot” indicator into a reusable ToolbarButton errorBadge prop and applies it to the UI mode output toggle.
  • Updates UI-mode tests to assert copying uses the original (unformatted) request body and validates the new request-body toggle behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/playwright-test/ui-mode-test-network-tab.spec.ts Extends request-body formatting tests and adjusts copy-request expectations to use raw body.
packages/web/src/components/toolbarButton.tsx Adds errorBadge support to show an error indicator on toolbar buttons.
packages/web/src/components/toolbarButton.css Styles the new toolbar-button error badge and makes buttons positioning-relative.
packages/web/src/components/toolbar.css Updates shadow styling selector to exclude .no-shadow toolbars via :not(...).
packages/trace-viewer/src/ui/uiModeView.tsx Switches output error indicator to the new ToolbarButton errorBadge prop.
packages/trace-viewer/src/ui/networkResourceDetails.tsx Adds pretty-print toggles for request/response bodies and centralizes formatting logic in useFormattedBody.
packages/trace-viewer/src/ui/networkResourceDetails.css Adds response bottom toolbar styling and response-body sizing rule.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +191 to +193
// Untoggle pretty print to see original request body
await payloadPanel.getByRole('button', { name: 'Pretty print', exact: true }).click();
await expect(payloadPanel.locator('.CodeMirror-code .CodeMirror-line')).toHaveText([
Comment on lines +324 to +332
function formatBody(body: string, contentType?: string): string {
if (!contentType)
return body;

const bodyStr = body;
if (bodyStr === '')
return '<Empty>';
if (isJsonMimeType(contentType))
return JSON.stringify(JSON.parse(body), null, 2);

if (isJsonMimeType(contentType)) {
try {
return JSON.stringify(JSON.parse(bodyStr), null, 2);
} catch (err) {
return bodyStr;
}
}

if (isXmlMimeType(contentType)) {
try {
return formatXml(bodyStr);
} catch {
return bodyStr;
}
}
if (isXmlMimeType(contentType))
return formatXml(body);
Comment on lines +78 to +80
.network-response-body {
height: calc(100% - 31px /* Height of bottom toolbar */);
}
Comment on lines +235 to +248
const [showFormattedResponse, setShowFormattedResponse] = useSetting('trace-viewer-network-details-show-formatted-response', true);
const formatResult = useFormattedBody(responseBody, showFormattedResponse);

return <div className='vbox network-request-details-tab'>
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
{responseBody && responseBody.font && <FontPreview font={responseBody.font} />}
{responseBody && responseBody.dataUrl && <div><img draggable='false' src={responseBody.dataUrl} /></div>}
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
{responseBody && responseBody.text !== undefined && <div className='network-response-body'>
<CodeMirrorWrapper text={formatResult.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>
<Toolbar noShadow={true} noMinHeight={true} className='network-response-toolbar'>
<div style={{ margin: 'auto' }}></div>
<FormatToggleButton toggled={showFormattedResponse} error={formatResult.error} onToggle={() => setShowFormattedResponse(!showFormattedResponse)} />
</Toolbar>
</div>}
Comment on lines 58 to 63
aria-label={ariaLabel || title}
>
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children}
{errorBadge && <span className='toolbar-button-error-badge' title={errorBadge}></span>}
</button>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: See the formatted (JSON or XML) response in the trace viewer Network tab

2 participants